是否可以在可能的情况下反序列化为具有强类型成员的ExpandoObject(或Dictionary)?

时间:2016-05-06 15:34:28

标签: c# json.net

我有一个类似于这个简化示例的JSON输入。

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

我想要实现的是" hybrid"结果,我的意思是顶级ExpandoObject,有两个属性model1model2,但是model1会有一个强类型MyType(基于Json.NET类型信息。由于model2没有类型信息,它将是一个嵌套的ExpandoObject。这个逻辑在更深的嵌套级别也应该是相同的(请参阅我的更新) ,在这方面简化了这个例子。

我的问题是我无法实现"混合性"。一种方法我可以有一个完全强类型的结果(如果顶级对象是强类型的),另一种方式我可以有一个完全动态的结果(一切都是ExpandoObject,或者第三种方式我可以有一个JObject在这种情况下毫无意义。

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

更新

我刚刚尝试反序列化为通用IDictionary,这样我就能获得顶级子属性的强类型结果,这在技术上解决了我的例子。但是,在较低级别它仍然无法正常工作,并为无类型的子属性提供JObject结果。总的来说,它不是我真实用例的好解决方案。

3 个答案:

答案 0 :(得分:3)

问题是Json.NET的ExpandoObjectConverter根本不处理任何自己的元数据属性,例如"$type""id""$ref"

但是,由于Json.NET是开放源代码及其MIT许可证allows modification,因此最简单的解决方案可能是制作您自己的ExpandoObjectConverter副本,并根据您的需求调整它{3}}。您还需要复制一些低级JSON实用程序:

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

然后使用它:

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

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

原型Json.NET Deserialization into dynamic object with referencing

最后,在使用TypeNameHandling时,请注意fiddle中的这一注意事项:

  

当您的应用程序从外部源反序列化JSON时,应谨慎使用TypeNameHandling。使用非None以外的值进行反序列化时,应使用自定义SerializationBinder验证传入类型。

有关可能需要执行此操作的讨论,请参阅 Newtonsoft docs

答案 1 :(得分:0)

这是我将如何做到的:

void Main()
{
    var json = "{\r\n  \"model1\": {\r\n    \"$type\": \"MyType, MyAssembly\",\r\n    \"A\": 5\r\n  },\r\n  \"model2" +
        "\": {\r\n    \"C\": \"something\"\r\n}}";
    var result = JsonConvert.DeserializeObject<Result>(json);
}

public class Result
{
    public MyType Model1 { get; set; }
    public ExpandoObject Model2 { get; set;}
}
public class MyType { public int A { get; set;} }

您还可以为Result.Model2提供dynamic类型(允许您使用result.Model2.something等语法访问其属性)或JSON.NET&#39; {{1} },这是更面向JSON的。

但是,如果你说你不想要JObject这样的课程,但你希望JSON的Result能够确定具体的实例类型,您可以使用TypeNameHandling setting

$type

请注意,如果您允许客户端提供的JSON值在.NET环境中实例化任意类型,则会产生安全隐患。

答案 2 :(得分:0)

有点旧的线程,但是这是可以做的。
首先构造您的ExpandoObject。例如:

dynamic someObject = new ExpandoObject();
someObject.Name = "My Expando Object";
someObject.SomeProperty = 123;

然后我建议使用“ JsonConvert”来序列化此对象(我知道您想反过来做,但是请耐心等待。)
因此,让我们按照以下顺序将“ someObject”序列化为一个test.json文件:

JsonSerializerSettings settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All   // Or Use .Auto for light weigth output
};

File.WriteAllText(@"e:\test.json", JsonConvert.SerializeObject(someObject, settings));

现在,如果您打开生成的Json文件,您将能够看到此JsonConverter所需的确切语法。现在,您可以编写自己的Json并执行逆运算。即。将您自己的Json文件反序列化为动态对象,如下所示:

dynamic test = JsonConvert.DeserializeObject<ExpandoObject>(File.ReadAllText(@"e:\yourOwnJsonFile.json"), settings);

最后,为了访问对象的属性,请执行以下操作:

((dynamic)test.Name = "My Own Expando Object";