c# - 根据属性的运行时值选择性地序列化属性

标签 c# .net json serialization json.net

从根本上说,我想根据序列化时的值从生成的 Json 中包含或省略属性。

更具体地说,我有一个类型知道是否已为其分配了一个值,并且我只想序列化该类型的属性,如果已经被分配给它(所以我需要在运行时检查值)。我试图让我的 API 能够轻松检测“具有默认值”和“根本未指定”之间的区别。

自定义 JsonConverter 似乎不够;我试过了,我相信属性名称在调用转换器之前已经序列化了。就我而言,我什至想省略属性名称。

我研究过扩展 DefaultContractResolver,但 CreateProperty 和 CreateProperties(返回 JsonProperty 序列化元数据)仅采用被序列化的类型,因此我无法检查实例本身。通常,我在 DefaultContractResolver 上看不到任何允许我控制 if 实例被序列化的东西;也许我错过了。

我还想也许我需要创建一个 ContractResolver 来为我的类型返回一个自定义的 JsonObjectContract。但是,同样,我在 JsonObjectContract 上看不到任何基于实例做出决策的内容。

是否有实现我的目标的好方法?我只是缺少一些简单的东西吗?非常感谢您提供的任何帮助。由于 Json.NET 如此可扩展,我认为这不会太难。但我开始认为我在这里的杂草丛生。 :)

最佳答案

好的,在 Json.NET 源代码中挖掘了一段时间后,我终于得到了这个工作,它甚至会尊重 Json.NET 支持的 ShouldSerialize* 和 *Specified 成员。请注意:这肯定会变成杂草。

所以我意识到 DefaultContractResolver.CreateProperty 返回的 JsonProperty 类具有 ShouldSerialize 和 Converter 属性,它们允许我指定如果属性实例实际上应该被序列化,如果是这样,如何 去做。

不过,反序列化需要一些不同的东西。默认情况下,对于自定义类型,DefaultContractResolver.ResolveContract 将返回具有空 Converter 属性的 JsonObjectContract。为了正确反序列化我的类型,我需要在契约(Contract)适用于我的类型时设置 Converter 属性。

这是代码(删除了错误处理/等以保持尽可能小)。

首先是需要特殊处理的类型:

public struct Optional<T>
{
    public readonly bool ValueProvided;
    public readonly T Value;

    private Optional( T value )
    {
        this.ValueProvided = true;
        this.Value = value;
    }

    public static implicit operator Optional<T>( T value )
    {
        return new Optional<T>( value );
    }
}

在我们知道它应该被序列化之后,转换器会正确地序列化它:

public class OptionalJsonConverter<T> : JsonConverter
{
    public static OptionalJsonConverter<T> Instance = new OptionalJsonConverter<T>();

    public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
    {
        var optional = (Optional<T>)value; // Cast so we can access the Optional<T> members
        serializer.Serialize( writer, optional.Value );
    }

    public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
    {
        var valueType = objectType.GetGenericArguments()[ 0 ];
        var innerValue = (T)serializer.Deserialize( reader, valueType );
        return (Optional<T>)innerValue; // Explicitly invoke the conversion from T to Optional<T>
    }

    public override bool CanConvert( Type objectType )
    {
        return objectType == typeof( Optional<T> );
    }
}

最后,也是最详细的,这是插入钩子(Hook)的 ContractResolver:

public class CustomContractResolver : DefaultContractResolver
{
    // For deserialization. Detect when the type is being deserialized and set the converter for it.
    public override JsonContract ResolveContract( Type type )
    {
        var contract = base.ResolveContract( type );
        if( contract.Converter == null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof( Optional<> ) )
        {
            // This may look fancy but it's just calling GetOptionalJsonConverter<T> with the correct T
            var optionalValueType = type.GetGenericArguments()[ 0 ];
            var genericMethod = this.GetAndMakeGenericMethod( "GetOptionalJsonConverter", optionalValueType );
            var converter = (JsonConverter)genericMethod.Invoke( null, null );
            // Set the converter for the type
            contract.Converter = converter;
        }
        return contract;
    }

    public static OptionalJsonConverter<T> GetOptionalJsonConverter<T>()
    {
        return OptionalJsonConverter<T>.Instance;
    }

    // For serialization. Detect when we're creating a JsonProperty for an Optional<T> member and modify it accordingly.
    protected override JsonProperty CreateProperty( MemberInfo member, MemberSerialization memberSerialization )
    {
        var jsonProperty = base.CreateProperty( member, memberSerialization );
        var type = jsonProperty.PropertyType;
        if( type.IsGenericType && type.GetGenericTypeDefinition() == typeof( Optional<> ) )
        {
            // This may look fancy but it's just calling SetJsonPropertyValuesForOptionalMember<T> with the correct T
            var optionalValueType = type.GetGenericArguments()[ 0 ];
            var genericMethod = this.GetAndMakeGenericMethod( "SetJsonPropertyValuesForOptionalMember", optionalValueType );
            genericMethod.Invoke( null, new object[]{ member.Name, jsonProperty } );
        }
        return jsonProperty;
    }

    public static void SetJsonPropertyValuesForOptionalMember<T>( string memberName, JsonProperty jsonProperty )
    {
        if( jsonProperty.ShouldSerialize == null ) // Honor ShouldSerialize*
        {
            jsonProperty.ShouldSerialize =
                ( declaringObject ) =>
                {
                    if( jsonProperty.GetIsSpecified != null && jsonProperty.GetIsSpecified( declaringObject ) ) // Honor *Specified
                    {
                        return true;
                    }                    
                    object optionalValue;
                    if( !TryGetPropertyValue( declaringObject, memberName, out optionalValue ) &&
                        !TryGetFieldValue( declaringObject, memberName, out optionalValue ) )
                    {
                        throw new InvalidOperationException( "Better error message here" );
                    }
                    return ( (Optional<T>)optionalValue ).ValueProvided;
                };
        }
        if( jsonProperty.Converter == null )
        {
            jsonProperty.Converter = CustomContractResolver.GetOptionalJsonConverter<T>();
        }
    }

    // Utility methods used in this class
    private MethodInfo GetAndMakeGenericMethod( string methodName, params Type[] typeArguments )
    {
        var method = this.GetType().GetMethod( methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static );
        return method.MakeGenericMethod( typeArguments );
    }

    private static bool TryGetPropertyValue( object declaringObject, string propertyName, out object value )
    {
        var propertyInfo = declaringObject.GetType().GetProperty( propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance );
        if( propertyInfo == null )
        {
            value = null;
            return false;
        }
        value = propertyInfo.GetValue( declaringObject, BindingFlags.GetProperty, null, null, null );
        return true;
    }

    private static bool TryGetFieldValue( object declaringObject, string fieldName, out object value )
    {
        var fieldInfo = declaringObject.GetType().GetField( fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance );
        if( fieldInfo == null )
        {
            value = null;
            return false;
        }
        value = fieldInfo.GetValue( declaringObject );
        return true;
    }
}

希望对其他人有所帮助。如果有任何不清楚的地方或我似乎遗漏了什么,请随时提出问题。

关于c# - 根据属性的运行时值选择性地序列化属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12522000/

相关文章:

c# - 有没有办法使 .NET Cast<T> 扩展方法使用用户定义的隐式转换运算符?

c# - 符号 <> 在 MSIL 中是什么意思?

javascript - 将内部 json 数组与空格组合,将外部数组与换行符组合

javascript - 从 foreach 创建动态 JSON

c# - 如何将 RTF 文件转换为 pdf 文件?

c# - 是否可以创建一个可以从两个线程访问的 GUI 控件?

c# - 解析 dd/MM/yyyy 格式时出现日期时间解析错误。字符串未被识别为有效的日期时间

c# - WebForms UnobtrusiveValidationMode 需要一个用于 jquery 的 ScriptResourceMapping

c# - Process.Start 使用 UAC on 的不同凭据

ios - 过滤其他 NSDictionary 中的 NSDictionary