如何使用JSON.NET将数据反序列化成嵌套/递归字典和列表?

56

我需要将一个复杂的JSON块反序列化为标准.NET容器,以供代码使用,该代码不了解JSON。它希望东西以标准.NET类型存在,特别是Dictionary<string, object>List<object>,其中"object"可以是基元或递归(字典或列表)。

我无法使用静态类型来映射结果,JObject/JToken也不适用。理想情况下,会有一些方法(通过合同可能?)将原始JSON转换为基本的.NET容器。

我已经在各处搜索了任何一种方法,试图诱导JSON.NET反序列化程序在遇到“{}”或“[]”时创建这些简单类型,但取得了很少的成功。

感谢任何帮助!


我尝试了System.Web.Script.Serialization.JavaScriptSerializer,它在这种情况下做到了我想要的,但是我有其他原因希望坚持使用JSON.NET。 - dongryphon
更新:目前我所做的是下载并修改Json.NET源代码中的CreateJObject和CreateJToken方法,以创建我想要的类型。有8-10个单元测试需要修复,但我可以接受最终的妥协方案。 - dongryphon
就此问题而言,它源于JsonSerializerInternalReader中HasDefinedType方法的用户。在咨询有关如何创建目标对象的合同之前,HasDefinedType检查是在之前进行的,即使它尝试了那样做,决策也已经在知道"{}"或"[]"是否在使用之前做出。我认为需要进行一些重构,以便Json.NET将此决策外部化,并允许用户代码在只知道"object"时确定目标类型。 - dongryphon
1
为什么@brian-rogers的回答没有被接受为最佳答案? - Ignacio Calvo
@IgnacioCalvo:因为问题明显是针对旧版本的Netwonsoft.Json,该版本还无法实现此功能。 - Joshua
5个回答

75

如果您只想要一种通用的方法,可以处理任意的JSON并将其转换为常规.NET类型(基本类型、列表和字典)的嵌套结构,则可以使用JSON.Net的LINQ-to-JSON API来实现:

using System.Linq;
using Newtonsoft.Json.Linq;

public static class JsonHelper
{
    public static object Deserialize(string json)
    {
        return ToObject(JToken.Parse(json));
    }

    public static object ToObject(JToken token)
    {
        switch (token.Type)
        {
            case JTokenType.Object:
                return token.Children<JProperty>()
                            .ToDictionary(prop => prop.Name,
                                          prop => ToObject(prop.Value));

            case JTokenType.Array:
                return token.Select(ToObject).ToList();

            default:
                return ((JValue)token).Value;
        }
    }
}
您可以按照以下示例调用方法。 根据您开始使用的JSON,obj将包含Dictionary<string,object>List<object>或原始类型。
object obj = JsonHelper.Deserialize(jsonString);

希望你不介意。我使用LINQ编辑了代码,使之更加简洁。 - bradgonesurfing
@bradgonesurfing 我喜欢它! - Brian Rogers
你如何在VB.NET中选择token.Select行? - NullVoxPopuli
我完全复制了这段代码,但是遇到了两个错误... 'Newtonsoft.Json.Linq.JEnumerable<Newtonsoft.Json.Linq.JProperty>' 没有定义 'ToDictionary',也没有找到接受类型为 'Newtonsoft.Json.Linq.JEnumerable<Newtonsoft.Json.Linq.JProperty>' 的第一个参数的扩展方法 'ToDictionary' (是否缺少 using 指令或程序集引用?) (还有一个类似于 JToken.Select 的错误) 我正在使用 Newtonsoft.Json; 和 using Newtonsoft.Json.Linq; -- 有什么想法吗?我不是在正确的 C# 版本上吗?(使用 4.0)。 - Jimmy Huch
3
你需要添加 using System.Linq - Brian Rogers
显示剩余6条评论

18

使用JSON.NET将json字符串递归反序列化为字典和列表的一种方法是创建一个自定义的json转换器类,该类派生自JSON.NET提供的JsonConverter抽象类。

在您的派生JsonConverter中,您可以实现对象如何读写json的功能。

您可以像这样使用自定义的JsonConverter

var o = JsonConvert.DeserializeObject<IDictionary<string, object>>(json, new DictionaryConverter());

以下是我过去使用成功的自定义Json转换器,可以实现您在问题中概述的相同目标:

public class DictionaryConverter : JsonConverter {
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { this.WriteValue(writer, value); }

    private void WriteValue(JsonWriter writer, object value) {
        var t = JToken.FromObject(value);
        switch (t.Type) {
            case JTokenType.Object:
                this.WriteObject(writer, value);
                break;
            case JTokenType.Array:
                this.WriteArray(writer, value);
                break;
            default:
                writer.WriteValue(value);
                break;
        }
    }

    private void WriteObject(JsonWriter writer, object value) {
        writer.WriteStartObject();
        var obj = value as IDictionary<string, object>;
        foreach (var kvp in obj) {
            writer.WritePropertyName(kvp.Key);
            this.WriteValue(writer, kvp.Value);
        }
        writer.WriteEndObject();
    }

    private void WriteArray(JsonWriter writer, object value) {
        writer.WriteStartArray();
        var array = value as IEnumerable<object>;
        foreach (var o in array) {
            this.WriteValue(writer, o);
        }
        writer.WriteEndArray();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
        return ReadValue(reader);
    }

    private object ReadValue(JsonReader reader) {
        while (reader.TokenType == JsonToken.Comment) {
            if (!reader.Read()) throw new JsonSerializationException("Unexpected Token when converting IDictionary<string, object>");
        }

        switch (reader.TokenType) {
            case JsonToken.StartObject:
                return ReadObject(reader);
            case JsonToken.StartArray:
                return this.ReadArray(reader);
            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 reader.Value;
            default:
                throw new JsonSerializationException
                    (string.Format("Unexpected token when converting IDictionary<string, object>: {0}", reader.TokenType));
        }
    }

    private object ReadArray(JsonReader reader) {
        IList<object> list = new List<object>();

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

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

        throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
    }

    private object ReadObject(JsonReader reader) {
        var obj = new Dictionary<string, object>();

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

                    if (!reader.Read()) {
                        throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
                    }

                    var v = ReadValue(reader);

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

        throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
    }

    public override bool CanConvert(Type objectType) { return typeof(IDictionary<string, object>).IsAssignableFrom(objectType); }
}

以下是 f# 的等效代码:

type IDictionaryConverter() =
    inherit JsonConverter()

    let rec writeValue (writer: JsonWriter) (value: obj) =
            let t = JToken.FromObject(value)
            match t.Type with
            | JTokenType.Object -> writeObject writer value
            | JTokenType.Array -> writeArray writer value
            | _ -> writer.WriteValue value    

    and writeObject (writer: JsonWriter) (value: obj) =
        writer.WriteStartObject ()
        let obj = value :?> IDictionary<string, obj>
        for kvp in obj do
            writer.WritePropertyName kvp.Key
            writeValue writer kvp.Value
        writer.WriteEndObject ()    

    and writeArray (writer: JsonWriter) (value: obj) = 
        writer.WriteStartArray ()
        let array = value :?> IEnumerable<obj>
        for o in array do
            writeValue writer o
        writer.WriteEndArray ()

    let rec readValue (reader: JsonReader) =
        while reader.TokenType = JsonToken.Comment do
            if reader.Read () |> not then raise (JsonSerializationException("Unexpected token when reading object"))

        match reader.TokenType with
        | JsonToken.Integer
        | JsonToken.Float
        | JsonToken.String
        | JsonToken.Boolean
        | JsonToken.Undefined
        | JsonToken.Null
        | JsonToken.Date
        | JsonToken.Bytes -> reader.Value
        | JsonToken.StartObject -> readObject reader Map.empty
        | JsonToken.StartArray -> readArray reader []
        | _ -> raise (JsonSerializationException(sprintf "Unexpected token when reading object: %O" reader.TokenType))


    and readObject (reader: JsonReader) (obj: Map<string, obj>) =
        match reader.Read() with
        | false -> raise (JsonSerializationException("Unexpected end when reading object"))
        | _ -> reader.TokenType |> function
            | JsonToken.Comment -> readObject reader obj
            | JsonToken.PropertyName ->
                let propertyName = reader.Value.ToString ()
                if reader.Read() |> not then raise (JsonSerializationException("Unexpected end when reading object"))
                let value = readValue reader
                readObject reader (obj.Add(propertyName, value))
            | JsonToken.EndObject -> box obj
            | _ -> raise (JsonSerializationException(sprintf "Unexpected token when reading object: %O" reader.TokenType))

    and readArray (reader: JsonReader) (collection: obj list) =
        match reader.Read() with
        | false -> raise (JsonSerializationException("Unexpected end when reading array"))
        | _ -> reader.TokenType |> function
            | JsonToken.Comment -> readArray reader collection
            | JsonToken.EndArray -> box collection
            | _ -> collection @ [readValue reader] |> readArray reader

    override __.CanConvert t = (typeof<IDictionary<string, obj>>).IsAssignableFrom t
    override __.WriteJson (writer:JsonWriter, value: obj, _:JsonSerializer) = writeValue writer value 
    override __.ReadJson (reader:JsonReader, _: Type, _:obj, _:JsonSerializer) = readValue reader

谢谢你。这解决了一半的挑战。第二个问题是要使用MatLab MWArray派生对象,而不是C#对象。在这里,我可以访问每个值及其属性名称,所以从这里开始似乎是一条直路。 - Mariusz

1

我喜欢AutoMapper,认为它可以解决很多问题...比如这个...

为什么不让JSON.NET将其转换为任何想要的格式...然后使用AutoMapper将其映射到你真正想要的对象中。

除非性能至关重要,否则这个额外步骤应该值得,因为可以减少复杂性并且可以使用你想要的序列化程序。


谢谢,我会研究一下。但我仍然希望能找到与JSON.NET本身相关的解决方案,因为它在遇到"{}"时需要创建一个"object",而在遇到"[]"时需要创建一个"object"。我只是不知道如何控制它在这种情况下创建的对象类型。 - dongryphon

0

感谢您的建议。我需要处理基于JSON的对象反序列化: "{}" 需要创建一个 Dictionary[string, object],而 "[]" 需要创建一个 List[object] 或普通的 object[]。我不知道如何将 JsonCoverter 与此问题连接起来。在目标类型为 "object" 时,反序列化器甚至在使用合同之前似乎有一些硬编码逻辑。 - dongryphon
覆盖合同解析器以连接自定义转换器。 - smartcaveman
谢谢,但我已经尝试过了。当类型落入反序列化器中的“!HasDefinedType”检查时,合同不会被使用。请查看JsonSerializerInternalReader.cs并搜索HasDefinedType。您将在委托到合同之前看到对此方法的调用,如果类型为“object”,则会被此检查捕获。 - dongryphon

0

你不能做我所要求的事情。至少在我经过了大量的研究后,我无法找到解决方法。我不得不编辑Json.NET的源代码。


这个更改有没有被推回源代码或者在其他地方可用? - Maslow
1
我有一个类似的问题,即我的字典值有时包含数组(基本上是“[]”)。这是我第一次遇到这个问题,但我很难理解为什么这个问题还没有以一种通用的方式得到解决。这似乎是一个相当基本的问题的死胡同。 有人愿意加入并解释一下 JSON 反序列化(JSON.NET)的主要问题吗? 其他人是否能够对他们的 JSON 进行更好地控制和结构化,或者我们在这里漏掉了什么? - PandaWood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接