Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

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

c# - How to deserialize Newtonsoft Json.NET references to separate, individual instances

I have a piece of JSON that looks like this:

[
  {
    "$id": "1",
    "Name": "James",
    "BirthDate": "1983-03-08T00:00Z",
    "LastModified": "2012-03-21T05:40Z"
  },
  {
    "$ref": "1"
  }
]

As you can tell by the $ref, this JSON array contains the same Person (James), twice. The second time is a reference to the first.

I am wondering if there is a way to deserialize this JSON into an object that contains two copies of the James person.

Currently, I'm using this:

var jsonSerializerSettings = new JsonSerializerSettings()
{
     PreserveReferencesHandling = PreserveReferencesHandling.None,
     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};

var deserializedPersons = JsonConvert.DeserializeObject<List<Person>>(json, jsonSerializerSettings);

But this just gives me an array with the same instance of the Person, twice:

object.ReferenceEquals(deserializedPersons[0], deserializedPersons[1]) // Evaluates to true

I've found a workaround I am unhappy with which is simply deserializing the JSON string, then serializing it using the jsonSerializerSettings above, which will duplicate the person in the JSON, then deserializing it again. This is causing major slowdowns for the large objects we are using.

Note: I know I could change the API that I retrieve this JSON from to duplicate the data, but preserving the references saves substantial space when sending the response JSON over the wire.

See Question&Answers more detail:os

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

1 Reply

0 votes
by (71.8m points)

You could use a custom reference resolver. For example, assuming Name is the "primary key" for your objects, this should work. Of course, you may want to make it more generic.

public class PersonReferenceResolver : IReferenceResolver
{
    private readonly IDictionary<string, Person> _objects = 
        new Dictionary<string, Person>();

    public object ResolveReference(object context, string reference)
    {
        Person p;
        if (_objects.TryGetValue(reference, out p))
        {
            //This is the "clever" bit. Instead of returning the found object
            //we just return a copy of it.
            //May be better to clone your class here...
            return new Person
            {
                Name = p.Name,
                BirthDate = p.BirthDate,
                LastModified = p.LastModified
            };
        }

        return null;
    }

    public string GetReference(object context, object value)
    {
        Person p = (Person)value;
        _objects[p.Name] = p;

        return p.Name;
    }

    public bool IsReferenced(object context, object value)
    {
        Person p = (Person)value;

        return _objects.ContainsKey(p.Name);
    }

    public void AddReference(object context, string reference, object value)
    {
        _objects[reference] = (Person)value;
    }
}

Now you deserialise like this:

var jsonSerializerSettings = new JsonSerializerSettings()
{
    ReferenceResolver = new PersonReferenceResolver()
};

var deserializedPersons = JsonConvert.DeserializeObject<List<Person>>(
    json, jsonSerializerSettings);

Edit: I was bored so I made a generic version:

public class GenericResolver<TEntity> : IReferenceResolver
    where TEntity : ICloneable, new()
{
    private readonly IDictionary<string, TEntity> _objects = new Dictionary<string, TEntity>();
    private readonly Func<TEntity, string> _keyReader;

    public GenericResolver(Func<TEntity, string> keyReader)
    {
        _keyReader = keyReader;
    }

    public object ResolveReference(object context, string reference)
    {
        TEntity o;
        if (_objects.TryGetValue(reference, out o))
        {
            return o.Clone();
        }

        return null;
    }

    public string GetReference(object context, object value)
    {
        var o = (TEntity)value;
        var key = _keyReader(o);
        _objects[key] = o;

        return key;
    }

    public bool IsReferenced(object context, object value)
    {
        var o = (TEntity)value;
        return _objects.ContainsKey(_keyReader(o));
    }

    public void AddReference(object context, string reference, object value)
    {
        if(value is TEntity)
            _objects[reference] = (TEntity)value;
    }
}

With slightly new usage:

var jsonSerializerSettings = new JsonSerializerSettings()
{
    //Now we need to specify the type and how to get the object's key
    ReferenceResolver = new GenericResolver<Person>(p => p.Name)
};

var deserializedPersons = JsonConvert.DeserializeObject<List<Person>>(
    json, jsonSerializerSettings);

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...