Serializing heavily linked data in .NET (customizing JSON.NET references)

前端 未结 1 1577
南方客
南方客 2020-12-10 07:09

I want to avoid reinventing the wheel when serializing data. I know some ways to serialize objects which are linked to each other, but it ranges from writing some code to wr

1条回答
  •  囚心锁ツ
    2020-12-10 07:39

    Solved the problem using JSON.NET (fantastic library!). Now objects are, first, serialized and referenced exactly where I want them them to; and second, without numerous "$id" and "$ref" fields. In my solution, the first property of an object is used as its identifier.

    I've created two JsonConvertors (for references to objects and for referenced objects):

    interface IJsonLinkable
    {
        string Id { get; }
    }
    
    class JsonRefConverter : JsonConverter
    {
        public override void WriteJson (JsonWriter writer, object value, JsonSerializer serializer)
        {
            writer.WriteValue(((IJsonLinkable)value).Id);
        }
    
        public override object ReadJson (JsonReader reader, Type type, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType != JsonToken.String)
                throw new Exception("Ref value must be a string.");
            return JsonLinkedContext.GetLinkedValue(serializer, type, reader.Value.ToString());
        }
    
        public override bool CanConvert (Type type)
        {
            return type.IsAssignableFrom(typeof(IJsonLinkable));
        }
    }
    
    class JsonRefedConverter : JsonConverter
    {
        public override void WriteJson (JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer, value);
        }
    
        public override object ReadJson (JsonReader reader, Type type, object existingValue, JsonSerializer serializer)
        {
            var jo = JObject.Load(reader);
            var value = JsonLinkedContext.GetLinkedValue(serializer, type, (string)jo.PropertyValues().First());
            serializer.Populate(jo.CreateReader(), value);
            return value;
        }
    
        public override bool CanConvert (Type type)
        {
            return type.IsAssignableFrom(typeof(IJsonLinkable));
        }
    }
    

    and a context to hold references data (with a dictionary for each type, so IDs need to be unique only among objects of the same type):

    class JsonLinkedContext
    {
        private readonly IDictionary> links = new Dictionary>();
    
        public static object GetLinkedValue (JsonSerializer serializer, Type type, string reference)
        {
            var context = (JsonLinkedContext)serializer.Context.Context;
            IDictionary links;
            if (!context.links.TryGetValue(type, out links))
                context.links[type] = links = new Dictionary();
            object value;
            if (!links.TryGetValue(reference, out value))
                links[reference] = value = FormatterServices.GetUninitializedObject(type);
            return value;
        }
    }
    

    Some attributes on the properties are necessary:

    [JsonObject(MemberSerialization.OptIn)]
    class Family
    {
        [JsonProperty(ItemConverterType = typeof(JsonRefedConverter))]
        public List persons;
    }
    
    [JsonObject(MemberSerialization.OptIn)]
    class Person : IJsonLinkable
    {
        [JsonProperty]
        public string name;
        [JsonProperty]
        public Pos pos;
        [JsonProperty, JsonConverter(typeof(JsonRefConverter))]
        public Person mate;
        [JsonProperty(ItemConverterType = typeof(JsonRefConverter))]
        public List children;
    
        string IJsonLinkable.Id { get { return name; } }
    }
    
    [JsonObject(MemberSerialization.OptIn)]
    class Pos
    {
        [JsonProperty]
        public int x;
        [JsonProperty]
        public int y;
    }
    

    So, when I serialize and deserialize using this code:

    JsonConvert.SerializeObject(family, Formatting.Indented, new JsonSerializerSettings {
        NullValueHandling = NullValueHandling.Ignore,
        Context = new StreamingContext(StreamingContextStates.All, new JsonLinkedContext()),
    });
    
    JsonConvert.DeserializeObject(File.ReadAllText(@"..\..\Data\Family.json"), new JsonSerializerSettings {
        Context = new StreamingContext(StreamingContextStates.All, new JsonLinkedContext()),
    });
    

    I get this neat JSON:

    {
      "persons": [
        {
          "name": "mom",
          "pos": {
            "x": 3,
            "y": 7
          },
          "mate": "dad",
          "children": [
            "bro",
            "sis"
          ]
        },
        {
          "name": "dad",
          "pos": {
            "x": 4,
            "y": 8
          },
          "mate": "mom",
          "children": [
            "bro",
            "sis"
          ]
        },
        {
          "name": "bro",
          "pos": {
            "x": 1,
            "y": 5
          }
        },
        {
          "name": "sis",
          "pos": {
            "x": 2,
            "y": 6
          }
        }
      ]
    }
    

    What I don't like in my solution, is that I have to use JObject, even though technically it's unnecessary. It probably creates quite a bit of objects, so loading will be slower. But looks like this is the most widely used approach for customizing convertors of objects. Methods which could be used to avoid this are private anyway.

    0 讨论(0)
提交回复
热议问题