Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optionally serialize a property based on its runtime value

Fundamentally, I want to include or omit a property from the generated Json based on its value at the time of serialization.

More-specifically, I have a type that knows if a value has been assigned to it and I only want to serialize properties of that type if there has been something assigned to it (so I need to inspect the value at runtime). I'm trying to make it easy for my API to detect the difference between "has the default value" and "wasn't specified at all".

A custom JsonConverter does not seem sufficient; I tried it and I believe the property name is already serialized before the converter is called. In my case I want to omit even the property name.

I've looked at extending DefaultContractResolver but CreateProperty and CreateProperties (which return JsonProperty serialization metadata) take only the Type being serialized, so I can't inspect the instance itself. In general, I don't see anything on the DefaultContractResolver allowing me to control if an instance is serialized; maybe I missed it.

I also thought maybe I needed to create a ContractResolver that returned a custom JsonObjectContract for my type. But, again, I don't see anything on JsonObjectContract that makes decisions based on an instance.

Is there a good way to accomplish my goal? Am I just missing something simple? Any help you can provide is greatly appreciated. Since Json.NET is so extensible, I thought this wouldn't be too hard. But I'm starting to think I'm way off in the weeds here. :)

like image 810
Amber Scouras Avatar asked Sep 20 '12 22:09

Amber Scouras


2 Answers

Ok, after digging around in Json.NET source for a while, I finally got this working and it will even honor the ShouldSerialize* and *Specified members that Json.NET supports. Be warned: this is definitely going off into the weeds.

So I realized that the JsonProperty class returned by DefaultContractResolver.CreateProperty has ShouldSerialize and Converter properties, which allow me to specify if the property instance should actually be serialized and, if so, how to do it.

Deserialization requires something a little different, though. DefaultContractResolver.ResolveContract will, by default for a custom type, return a JsonObjectContract with a null Converter property. In order to deserialize my type properly, I needed to set the Converter property when the contract is for my type.

Here's the code (with error handling / etc removed to keep things as small as possible).

First, the type that needs special handling:

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 );
    }
}

And there's the converter that will serialize it properly after we know it should be serialized:

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> );
    }
}

Finally, and most-verbosely, here's the ContractResolver that inserts the hooks:

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;
    }
}

I hope that helps somebody else. Feel free to ask questions if anything is unclear or if it looks like I missed something.

like image 194
Amber Scouras Avatar answered Nov 17 '22 05:11

Amber Scouras


Because you are asking to do it based on the value of a property what you can do is put the data into a dictionary. You can exclude adding the value to the dictionary. Below is a simple example of how to get the data out of an object.

public class Class1
{
    public string Name { get; set; }
}

[TestFixture]
public class Tests
{
    [Test]
    public void ConvertTest()
    {
        var dictionary = new Dictionary<string, object>();
        var @class = new Class1 { Name = "Joe" };

        var propertyInfos = typeof (Class1).GetProperties();

        foreach (PropertyInfo propertyInfo in propertyInfos)
        {
            dictionary.Add(propertyInfo.Name, propertyInfo.GetValue(@class, BindingFlags.GetProperty, null, null, null));
        }

        var serializeObject = JsonConvert.SerializeObject(dictionary);
        var o = JsonConvert.SerializeObject(@class);

        Console.WriteLine(serializeObject);
        Console.WriteLine(o);

        var class1 = JsonConvert.DeserializeObject<Class1>(serializeObject);
        Console.WriteLine(class1.Name);
    }
}
like image 4
Lance Avatar answered Nov 17 '22 05:11

Lance