2

I've read the docs on json.net pretty well, and I'm running out of ideas. I have a situation where I have children with references to there parents. So

public class ObjA
{
  public int Id {get;set}
  public string OtherStuff {get;set}
  public ObjB MyChild {get;set}
}

public class ObjB
{
  public int Id {get;set}
  public string OtherStuff {get;set}
  public ObjA MyParent {get;set}
}

This wont serialize. So I could do [JsonIgnore] but what I'd rather do is do [JsonIdOnly] where ObjB, instead of having a serialization of MyParent or skipping MyParent entirely shows a json property for MyParentId:123. So

{OjbA:{
    Id:123,
    OtherStuff:some other stuff,
    MyChild:{
        Id:456,
        OtherStuff:Some other stuff,
        MyParentId:123,
        }
    }
 }

I know I can write a custom converter for a type. THe problem is that I want this to happen only when designated because otherwise I would not be able to serialize ObjA. In other words I need this to happen only when i decorate it with an attribute. So

public class ObjB
{
  public int Id {get;set}
  public string OtherStuff {get;set}
  [JsonIdOnly]
  public ObjA MyParent {get;set}
}
ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
Raif
  • 8,641
  • 13
  • 45
  • 56

2 Answers2

2

If you do not care that your JSON has some extra bookkeeping info in it, then set the PreserveReferenceHandling option in JsonSerializerSettings to All (or Objects), as @Athari suggested. That is the easiest way to make it work. If you do that, your JSON would look like this:

{
  "$id": "1",
  "Id": 123,
  "OtherStuff": "other stuff A",
  "MyChild": {
    "$id": "2",
    "Id": 456,
    "OtherStuff": "other stuff B",
    "MyParent": {
      "$ref": "1"
    }
  }
}

That said, there is a way to do what you originally wanted, using a custom JsonConverter. What you can do is make a converter that will accept any object that has an Id property. Then, for those places where you want it serialized only as an Id, you can decorate those properties with the [JsonConverter] attribute. The custom converter will then be used for those cases, but not otherwise. Here's what the converter might look like:

class IdOnlyConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType.IsClass;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteStartObject();
        writer.WritePropertyName("Id");
        writer.WriteValue(GetId(value));
        writer.WriteEndObject();
    }

    private int GetId(object obj)
    {
        PropertyInfo prop = obj.GetType().GetProperty("Id", typeof(int));
        if (prop != null && prop.CanRead)
        {
            return (int)prop.GetValue(obj, null);
        }
        return 0;
    }

    public override bool CanRead 
    { 
        get { return false; } 
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

To use the converter, you'd set up your classes just like you outlined. Notice how MyParent is decorated with an attribute to tell Json.Net to use the custom converter for that property.

public class ObjA
{
    public int Id { get; set; }
    public string OtherStuff { get; set; }
    public ObjB MyChild { get; set; }
}

public class ObjB
{
    public int Id { get; set; }
    public string OtherStuff { get; set; }
    [JsonConverter(typeof(IdOnlyConverter))]
    public ObjA MyParent { get; set; }
}

When serializing, you will need to set the ReferenceLoopHandling option of JsonSerializerSettings to Serialize to tell Json.Net not to throw an error if a reference loop is detected, and to continue serializing anyway (since our converter will handle it).

Putting it all together, here is some example code demonstrating the converter in action:

class Program
{
    static void Main(string[] args)
    {
        ObjA a = new ObjA();
        a.Id = 123;
        a.OtherStuff = "other stuff A";

        ObjB b = new ObjB();
        b.Id = 456;
        b.OtherStuff = "other stuff B";
        b.MyParent = a;

        a.MyChild = b;

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize,
            Formatting = Newtonsoft.Json.Formatting.Indented
        };

        string json = JsonConvert.SerializeObject(a, settings);
        Console.WriteLine(json);
    }
}

And here is the output of the above:

{
  "Id": 123,
  "OtherStuff": "other stuff A",
  "MyChild": {
    "Id": 456,
    "OtherStuff": "other stuff B",
    "MyParent": {
      "Id": 123
    }
  }
}
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • oh, ok, yea I created a converter but didn't know how to get him into an attribute. This will work fine. Thanks to you both. – Raif Oct 03 '13 at 18:39
  • hi, so all's well except that I'm getting this error { "Message": "An error has occurred.", "ExceptionMessage": "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; charset=utf-8'.", "ExceptionType": "System.InvalidOperationException", "StackTrace": null, "InnerException": { "Message": "An error has occurred.", "ExceptionMessage": "Error creating GuidEntityToIdJsonConverter", "ExceptionType": "Newtonsoft.Json.JsonException", – Raif Oct 03 '13 at 19:09
  • well, ok, so at the end of that stacktrace there was an inner error that said "no parameterless constructor" which is usually code for "you will never ever find the error" but this time I just created a parameterless constructor and it worked. how about that? – Raif Oct 03 '13 at 19:22
  • Ah. Yes, I was just writing that the exception was saying that Json.Net could not instantiate your `GuidEntityToIdJsonConverter` for some reason. One possible reason is it does not have a parameterless constructor. (A parameterless constructor is required when using the converter from the `[JsonConverter]` attribute because Json.net would not know what to pass to other constructors in order to instantiate the converter.) – Brian Rogers Oct 03 '13 at 19:24
  • The IdOnlyConverter gave me problems when trying to deserialize the decorated field. It worked after I overrode the CanRead method: `public override bool CanRead { get { return false; } }` – user3533716 Sep 19 '16 at 15:17
  • @user3533716 Yes, sorry, that should have been in the code since ReadJson is not implemented. I updated the answer. Thanks for the comment. – Brian Rogers Sep 19 '16 at 16:31
0

You can change JsonSerializerSettings.PreserveReferencesHandling to PreserveReferencesHandling.Objects or PreserveReferencesHandling.All (you may also need to change JsonSerializerSettings.ReferenceLoopHandling and various JsonPropertyAttribute properties regarding references). This will add $ref JSON properties to objects and cross-links will be preserved.

You can modify the text of $ref properties using custom IReferenceResolver.

However, if you need more complex references resolving, like the one you have suggested, without $ref properties, then you'll have to write some code.

Athari
  • 33,702
  • 16
  • 105
  • 146
  • thank you for the response. I had not seen these properties and am investigating. However, I am more than happy to write some code to get what I want. I just couldn't figure it out from customConverter and converterAttribute. I'm reading up on the above props now (actually I'm going to lunch, but then) with an aim to figure out how to write my own extension. If you happen to know the right direction then it would be great to know. Thanks again, raif – Raif Oct 03 '13 at 16:45
  • really quick before I go to lunch it looks like the ReferenceLoopHandling is very close. If I could inherit from it or extend it via some mechanism that would be perfect. – Raif Oct 03 '13 at 16:52
  • If what your JSON looks like is not crucial, I suggest sticking with `$ref` properties. I've implemented custom references in [my private framework](https://github.com/Athari/Alba.Framework/tree/master/Alba.Framework.Serialization.Json), but I'm afraid it's too complex to be a good sample. It makes links like you suggested above possible, but it requires lots of attributes and a bit of custom code; and there're no docs. :) – Athari Oct 03 '13 at 16:58
  • @Raif Almost forgot! You can use this: http://stackoverflow.com/a/13113901/293099 It's the first version of what has grown into classes I've linked above. Objects have string identifiers, unique per type. – Athari Oct 03 '13 at 17:03