Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
378 views
in Technique[技术] by (71.8m points)

c# - Custom object serialization vs PreserveReferencesHandling

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 deserializing, when using a custom JsonConverter?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...