Custom object serialization vs PreserveReferencesHandling

后端 未结 1 1775
無奈伤痛
無奈伤痛 2020-12-21 10:38

Is there any standard way to get \"$id\" field\'s value for the current object when serializing, and get the object by its \"$id\" value when deser

相关标签:
1条回答
  • 2020-12-21 10:53

    Inside a custom JsonConverter you can use the IReferenceResolver returned by JsonSerializer.ReferenceResolver to manually read and write Json.NET's "$id" and "$ref" properties.

    The following converter provides a template for this:

    public abstract class ReferenceHandlingCustomCreationConverter<T> : JsonConverter where T : class
    {
        const string refProperty = "$ref";
        const string idProperty = "$id";
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(T).IsAssignableFrom(objectType);
        }
    
        protected virtual T Create(Type objectType, T existingValue, JsonSerializer serializer, JObject obj)
        {
            return existingValue ?? (T)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
        }
    
        protected abstract void Populate(JObject obj, T value, JsonSerializer serializer);
    
        protected abstract void WriteProperties(JsonWriter writer, T value, JsonSerializer serializer, JsonObjectContract contract);
    
        public override sealed object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var contract = serializer.ContractResolver.ResolveContract(objectType);
            if (!(contract is JsonObjectContract))
            {
                throw new JsonSerializationException(string.Format("Invalid non-object contract type {0}", contract));
            }
            if (!(existingValue == null || existingValue is T))
            {
                throw new JsonSerializationException(string.Format("Converter cannot read JSON with the specified existing value. {0} is required.", typeof(T)));
            }
    
            if (reader.MoveToContent().TokenType == JsonToken.Null)
                return null;
            var obj = JObject.Load(reader);
    
            var refId = (string)obj[refProperty].RemoveFromLowestPossibleParent();
            var objId = (string)obj[idProperty].RemoveFromLowestPossibleParent();
            if (refId != null)
            {
                var reference = serializer.ReferenceResolver.ResolveReference(serializer, refId);
                if (reference != null)
                    return reference;
            }
    
            var value = Create(objectType, (T)existingValue, serializer, obj);
    
            if (objId != null)
            {
                // Add the empty array into the reference table BEFORE poppulating it, to handle recursive references.
                serializer.ReferenceResolver.AddReference(serializer, objId, value);
            }
    
            Populate(obj, value, serializer);
    
            return value;
        }
    
        public override sealed void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var contract = serializer.ContractResolver.ResolveContract(value.GetType());
            if (!(contract is JsonObjectContract))
            {
                throw new JsonSerializationException(string.Format("Invalid non-object contract type {0}", contract));
            }
            if (!(value is T))
            {
                throw new JsonSerializationException(string.Format("Converter cannot read JSON with the specified existing value. {0} is required.", typeof(T)));
            }
    
            writer.WriteStartObject();
    
            if (serializer.ReferenceResolver.IsReferenced(serializer, value))
            {
                writer.WritePropertyName(refProperty);
                writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value));
            }
            else
            {
                writer.WritePropertyName(idProperty);
                writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value));
    
                WriteProperties(writer, (T)value, serializer, (JsonObjectContract)contract);
            }
    
            writer.WriteEndObject();
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonReader MoveToContent(this JsonReader reader)
        {
            if (reader.TokenType == JsonToken.None)
                reader.Read();
            while (reader.TokenType == JsonToken.Comment && reader.Read())
                ;
            return reader;
        }
    
        public static JToken RemoveFromLowestPossibleParent(this JToken node)
        {
            if (node == null)
                return null;
            var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
            if (contained != null)
                contained.Remove();
            // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
            if (node.Parent is JProperty)
                ((JProperty)node.Parent).Value = null;
            return node;
        }
    }
    

    Implementing the converter requires implementing the two abstract methods:

    protected abstract void Populate(JObject obj, T value, JsonSerializer serializer);
    
    protected abstract void WriteProperties(JsonWriter writer, T value, JsonSerializer serializer, JsonObjectContract contract);
    

    One default generic implementation might look like:

    public class DefaultReferenceHandlingCustomCreationConverter<T> : ReferenceHandlingCustomCreationConverter<T> where T : class
    {
        protected override void Populate(JObject obj, T value, JsonSerializer serializer)
        {
            using (var reader = obj.CreateReader())
                serializer.Populate(reader, value);
        }
    
        protected override void WriteProperties(JsonWriter writer, T value, JsonSerializer serializer, JsonObjectContract contract)
        {
            foreach (var property in contract.Properties.Where(p => p.Writable && !p.Ignored))
            {
                // TODO: handle JsonProperty attributes including
                // property.Converter, property.IsReference, property.ItemConverter, property.ItemReferenceLoopHandling, 
                // property.ItemReferenceLoopHandling, property.ObjectCreationHandling, property.ReferenceLoopHandling, property.Required                            
                var itemValue = property.ValueProvider.GetValue(value);
                writer.WritePropertyName(property.PropertyName);
                serializer.Serialize(writer, itemValue);
            }
        }
    }
    

    Using it, then you would serialize as follows:

    var settings = new JsonSerializerSettings
    {
        Converters = { new DefaultReferenceHandlingCustomCreationConverter<RootObject>() },
        ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
    };
    var json = JsonConvert.SerializeObject(parent, Formatting.Indented, settings);
    

    Notes:

    • The converter is designed to work with c# classes that are serialized as JSON objects. It would also be possible to create a converter that manually writes and reads "$ref", "$id" and "$values" properties for collections, e.g. as shown in this answer to Cannot preserve reference to array or readonly list, or list created from a non-default constructor.

    • The converter splits up the tasks of creating and populating the object during deserialization, and thus will not work with objects with only parameterized constructors. This is required to make recursive self-references resolve correctly.

    • Serializing with ReferenceLoopHandling.Serialize is required.

    Sample fiddle here.

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