LazyTarget
LazyTarget

Reputation: 879

Handling reference loops in JSON.net

I wish to serialize a collection (List<Item>) of items to JSON.

These items have a collection of Connection which gives information about the connection from one Item to a second Item. And since the connection object has a reference to the items it makes it an infinite loop.

My question is is there a way for me to skip serialization of the connection collection when serializing the object the second time.

I've tried things like inheriting from JsonConverter and writing a custom WriteJson() method but from there I have no sence whether I should write out the array or not.

I've also tried using a custom ContractResolver but with no good results.


Classes

public class Item
{
    private static int _lastID = 0;

    public Item()
    {
        ID = ++_lastID;
        Connections = new List<Connection>();
    }


    public int ID { get; set; }

    public string Name { get; set; }

    public string Prop1 { get; set; }

    public string Prop2 { get; set; }

    public List<Connection> Connections { get; set; }

}



public class Connection
{
    private Connection(ConnectionType type, Item source, Item target)
    {
        if (type == ConnectionType.None)
            throw new ArgumentException();
        if (source == null)
            throw new ArgumentNullException("source");
        if (target == null)
            throw new ArgumentNullException("target");

        Type = type;
        Source = source;
        Target = target;
    }


    public ConnectionType Type { get; set; }

    public Item Source { get; set; }

    public Item Target { get; set; }


    public static void Connect(ConnectionType type, Item source, Item target)
    {
        var conn = new Connection(type, source, target);
        source.Connections.Add(conn);
        target.Connections.Add(conn);
    }
}


Wanted result:

[
    {
        "id": 1,
        "name": "Item #1",
        "prop1": "val1",
        "prop2": "val2",
        "connections": {
            "type": "ConnType",
            "source": {
                "id": 1,
                "name": "Item #1",
                "prop1": "val1",
                "prop2": "val2"
                // no connections array
            },
            "target": {
                "id": 2,
                "name": "Item #2",
                "prop1": "val1",
                "prop2": "val2"
                // no connections array
            }
        }
    },
    {
        "id": 2,
        "name": "Item #2",
        "prop1": "val1",
        "prop2": "val2",
        "connections": {
            "type": "ConnType",
            "source": {
                "id": 1,
                "name": "Item #1",
                "prop1": "val1",
                "prop2": "val2"
                // no connections array
            },
            "target": {
                "id": 2,
                "name": "Item #2",
                "prop1": "val1",
                "prop2": "val2"
                // no connections array
            }
        }
    }
]



EDIT:

C#

var settings = new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
        Formatting = Formatting.Indented
    };
settings.Converters.Add(new StringEnumConverter());
var json = JsonConvert.SerializeObject(collection, settings);

Upvotes: 5

Views: 8909

Answers (4)

Paul Rabbit
Paul Rabbit

Reputation: 21

If I'm not mistaken you need to keep only the first depth of reference containing the connection collection? If that is the case, try using:

settings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
settings.MaxDepth = 1;

Upvotes: 1

Bas
Bas

Reputation: 27085

There is an issue, since objects can't be referenced in Json (see Standard way of referencing an object by identity (for, eg, circular references)?).

This means you have to duplicate an item for each time it occurs in a reference to bring all the connections to the client.

I suggest to bring the connections to the client using only ids, with the connections as a seperate object.

Add the JsonIgnore attribute to the Connections property on the Item.

[JsonIgnore]
public List<Connection> Connections { get; set; }

And use a class to send to the client instead of the list of items directly.

    class ConnectionContainer
    {
        private readonly List<Item> _items;
        private readonly List<ConnectionInfo> _connections;

        public ConnectionContainer(IEnumerable<Item> items)
        {
            _items = items.ToList();
            Connections = items.SelectMany(i => i.Connections).Distinct().Select(c => new ConnectionInfo
            {
                Type = c.Type,
                SourceId = c.Source.ID,
                TargetId = c.Target.ID
            }).ToList();
        }

        public List<Item> Items
        {
            get { return _items; }
        }

        public List<ConnectionInfo> Connections
        {
            get { return _connections; }
        }
    }

    class ConnectionInfo
    {
        private ConnectionType Type { get; set; }
        private int SourceId { get; set; }
        private int TargetId { get; set; }
    }

Upvotes: 0

Moeri
Moeri

Reputation: 9294

Add this to your Global.asax (or in the WebApiConfig or any other config class)

var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;

Upvotes: 2

Wasif Hossain
Wasif Hossain

Reputation: 3940

Instead of declaring the type of Source and Target as of Item, you may introduce a new class, say, ItemClass, which'll contain all the fields of class Item, except the Connections property.

public class ItemClass
{
    public int ID { get; set; }

    public string Name { get; set; }

    public string Prop1 { get; set; }

    public string Prop2 { get; set; }
}

public class Connection
{
    // ...

    public ConnectionType Type { get; set; }

    public ItemClass Source { get; set; }

    public ItemClass Target { get; set; }

    // ...
}

Now you'll have an overhead of populating the new ItemClass type instance accordingly.

Upvotes: 0

Related Questions