Search code examples
c#jsonserializationattributesjson.net

Json.Net custom serialization from attribute


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}
}

Solution

  • 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
        }
      }
    }