Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to deserialize into an ExpandoObject (or Dictionary) having strongly-typed members where possible?

Tags:

c#

json.net

I have a JSON input similar to this simplified example.

{
  "model1": {
    "$type": "MyType, MyAssembly",
    "A": 5
  },
  "model2": {
    "C": "something"
}

What I'd like to achieve is a "hybrid" result, which I mean a top-level ExpandoObject, having two properties model1 and model2, BUT model1 would have a strong type of MyType (based on the Json.NET type information. As model2 doesn't have type information, it would be a nested ExpandoObject. This logic should be the same across deeper nesting levels as well (see my update), the example is simplified in this regard.

My problem is that I can't achieve the "hybridness". One way I could have a completely strongly typed result (if the top-level object would be strongly typed), the other way I can have a completely dynamic result (everything is ExpandoObject, or the third way I could have a JObject which is meaningless in this scenario.

// this will give a fully dynamic result, regardless the child type information
var result = JsonConvert.DeserializeObject<ExpandoObject>(input, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });

UPDATE

I've just experimented with deserializing into a generic IDictionary, and that way I can get strongly typed results for top-level child properties, which technically solves my example. However, at lower levels it's still not working, and gives a JObject result for untyped child properties. So overall it's not a good solution for my real use case.

like image 491
Zoltán Tamási Avatar asked May 06 '16 15:05

Zoltán Tamási


1 Answers

The problem is that Json.NET's ExpandoObjectConverter simply does not handle any of its own metadata properties such as "$type", "id" or "$ref".

However, since Json.NET is open source and its MIT license allows modification, the easiest solution may be to make your own copy of ExpandoObjectConverter and adapt it to your needs, along the lines of Json.NET Deserialization into dynamic object with referencing. You'll need to copy some low-level JSON utilities as well:

/// <summary>
/// Converts an ExpandoObject to and from JSON.
/// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/ExpandoObjectConverter.cs
/// License: https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md
/// </summary>
public class TypeNameHandlingExpandoObjectConverter : JsonConverter
{
    /// <summary>
    /// Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // can write is set to false
    }

    /// <summary>
    /// Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return ReadValue(reader, serializer);
    }

    private object ReadValue(JsonReader reader, JsonSerializer serializer)
    {
        if (!reader.MoveToContent())
        {
            throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject.");
        }

        switch (reader.TokenType)
        {
            case JsonToken.StartObject:
                return ReadObject(reader, serializer);
            case JsonToken.StartArray:
                return ReadList(reader, serializer);
            default:
                if (JsonTokenUtils.IsPrimitiveToken(reader.TokenType))
                {
                    return reader.Value;
                }

                throw JsonSerializationExceptionHelper.Create(reader, string.Format("Unexpected token when converting ExpandoObject: {0}", reader.TokenType));
        }
    }

    private object ReadList(JsonReader reader, JsonSerializer serializer)
    {
        IList<object> list = new List<object>();

        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonToken.Comment:
                    break;
                default:
                    object v = ReadValue(reader, serializer);

                    list.Add(v);
                    break;
                case JsonToken.EndArray:
                    return list;
            }
        }

        throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject.");
    }

    private object ReadObject(JsonReader reader, JsonSerializer serializer)
    {
        if (serializer.TypeNameHandling != TypeNameHandling.None)
        {
            var obj = JObject.Load(reader);

            Type polymorphicType = null;
            var polymorphicTypeString = (string)obj["$type"];
            if (polymorphicTypeString != null)
            {
                if (serializer.TypeNameHandling != TypeNameHandling.None)
                {
                    string typeName, assemblyName;
                    ReflectionUtils.SplitFullyQualifiedTypeName(polymorphicTypeString, out typeName, out assemblyName);
                    polymorphicType = serializer.Binder.BindToType(assemblyName, typeName);
                }
                obj.Remove("$type");
            }

            if (polymorphicType == null || polymorphicType == typeof(ExpandoObject))
            {
                using (var subReader = obj.CreateReader())
                    return ReadExpandoObject(subReader, serializer);
            }
            else
            {
                using (var subReader = obj.CreateReader())
                    return serializer.Deserialize(subReader, polymorphicType);
            }
        }
        else
        {
            return ReadExpandoObject(reader, serializer);
        }
    }

    private object ReadExpandoObject(JsonReader reader, JsonSerializer serializer)
    {
        IDictionary<string, object> expandoObject = new ExpandoObject();

        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonToken.PropertyName:
                    string propertyName = reader.Value.ToString();

                    if (!reader.Read())
                    {
                        throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject.");
                    }

                    object v = ReadValue(reader, serializer);

                    expandoObject[propertyName] = v;
                    break;
                case JsonToken.Comment:
                    break;
                case JsonToken.EndObject:
                    return expandoObject;
            }
        }

        throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading ExpandoObject.");
    }

    /// <summary>
    /// Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(ExpandoObject));
    }

    /// <summary>
    /// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override bool CanWrite
    {
        get { return false; }
    }
}

internal static class JsonTokenUtils
{
    // Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/JsonTokenUtils.cs
    public static bool IsPrimitiveToken(this JsonToken token)
    {
        switch (token)
        {
            case JsonToken.Integer:
            case JsonToken.Float:
            case JsonToken.String:
            case JsonToken.Boolean:
            case JsonToken.Undefined:
            case JsonToken.Null:
            case JsonToken.Date:
            case JsonToken.Bytes:
                return true;
            default:
                return false;
        }
    }
}

internal static class JsonReaderExtensions
{
    // Adapted from internal bool JsonReader.MoveToContent()
    // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/JsonReader.cs#L1145
    public static bool MoveToContent(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        JsonToken t = reader.TokenType;
        while (t == JsonToken.None || t == JsonToken.Comment)
        {
            if (!reader.Read())
            {
                return false;
            }

            t = reader.TokenType;
        }

        return true;
    }
}

internal static class JsonSerializationExceptionHelper
{
    public static JsonSerializationException Create(this JsonReader reader, string format, params object[] args)
    {
        // Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/JsonPosition.cs

        var lineInfo = reader as IJsonLineInfo;
        var path = (reader == null ? null : reader.Path);
        var message = string.Format(CultureInfo.InvariantCulture, format, args);
        if (!message.EndsWith(Environment.NewLine, StringComparison.Ordinal))
        {
            message = message.Trim();
            if (!message.EndsWith(".", StringComparison.Ordinal))
                message += ".";
            message += " ";
        }
        message += string.Format(CultureInfo.InvariantCulture, "Path '{0}'", path);
        if (lineInfo != null && lineInfo.HasLineInfo())
            message += string.Format(CultureInfo.InvariantCulture, ", line {0}, position {1}", lineInfo.LineNumber, lineInfo.LinePosition);
        message += ".";

        return new JsonSerializationException(message);
    }
}

internal static class ReflectionUtils
{
    // Utilities taken from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/ReflectionUtils.cs
    // I couldn't find a way to access these directly.

    public static void SplitFullyQualifiedTypeName(string fullyQualifiedTypeName, out string typeName, out string assemblyName)
    {
        int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName);

        if (assemblyDelimiterIndex != null)
        {
            typeName = fullyQualifiedTypeName.Substring(0, assemblyDelimiterIndex.GetValueOrDefault()).Trim();
            assemblyName = fullyQualifiedTypeName.Substring(assemblyDelimiterIndex.GetValueOrDefault() + 1, fullyQualifiedTypeName.Length - assemblyDelimiterIndex.GetValueOrDefault() - 1).Trim();
        }
        else
        {
            typeName = fullyQualifiedTypeName;
            assemblyName = null;
        }
    }

    private static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName)
    {
        int scope = 0;
        for (int i = 0; i < fullyQualifiedTypeName.Length; i++)
        {
            char current = fullyQualifiedTypeName[i];
            switch (current)
            {
                case '[':
                    scope++;
                    break;
                case ']':
                    scope--;
                    break;
                case ',':
                    if (scope == 0)
                    {
                        return i;
                    }
                    break;
            }
        }

        return null;
    }
}

Then use it like:

var settings = new JsonSerializerSettings
{
    Formatting = Newtonsoft.Json.Formatting.Indented,
    TypeNameHandling = TypeNameHandling.Auto,
    Converters = new [] { new TypeNameHandlingExpandoObjectConverter() },
};

var expando2 = JsonConvert.DeserializeObject<ExpandoObject>(input, settings);

Prototype fiddle.

Finally, when using TypeNameHandling, do take note of this caution from the Newtonsoft docs:

TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.

For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json.

like image 96
dbc Avatar answered Sep 30 '22 12:09

dbc