Home > Net >  Custom JSON.Net JSON Converter with nested objects
Custom JSON.Net JSON Converter with nested objects

Time:12-01

Sourcecode to showcase the issue: https://github.com/Snuffsis/ConverterExample

So I have an issue that is exactly the same as in this stackoverflow question:

C# Newtonsoft.Json Custom Deserializer

And while that answer does help for properties that are simple types (int, bool, string etc) it doesn't work when there needs to be a nested object. As it throws an exception for Newtonsoft.Json.JsonSerializationException: Self referecing loop detected for property 'Value' with type ... where the type is the json object, in this case soBillingContact.

It should be able to handle these two JSON formats
Example:

{
  "printNoteOnInternalDocuments": {
    "value": true
  },
  "soBillingContact": {
    "value": {
      "overrideContact": {
        "value": true
      },
      "name": {
        "value": "string"
      },
      "attention": {
        "value": "string"
      },
      "email": {
        "value": "string"
      },
      "web": {
        "value": "string"
      },
      "phone1": {
        "value": "string"
      },
      "phone2": {
        "value": "string"
      },
      "fax": {
        "value": "string"
      }
    }
  }
}
{
  "printNoteOnInternalDocuments": true,
  "soBillingContact": {
    "overrideContact": true,
    "contactId": 0,
    "name": "string",
    "attention": "string",
    "email": "string",
    "web": "string",
    "phone1": "string",
    "phone2": "string",
    "fax": "string"
  }
}

The solution in the linked question works fine for the object itself if i create the object as a root. It's only when it's a nested object that it becomes a problem.

I am trying to avoid having to write a custom converter for each json object that exists, and instead try to make a generic one. Which is probably my issue and maybe should be abandoned. But just checking if anyone might have any ideas for a solution.

And aside from that solution above, I have written my own converters that does similar stuff which works fine. Along with custom converter for each specific nested objects, which also works fine.

This is the code that i made myself that works when its for a specific object: Main:

static void Main(string[] args)
{
  var vSalesOrder = new SalesOrder()
  {
    Project = 1,
    PrintDescriptionOnInvoice = true,
    PrintNoteOnExternalDocuments = true,
    SoBillingContact = new Contact
    {
      Attention = "attention",
      Email = "@whatever.se",
      Fax = "lolfax"
    }
  };

  var jsonString = JsonConvert.SerializeObject(vSalesOrder);
}

Expected output after this should have similar structure as the json above, except for the few properties that have been left out.

SalesOrder Class:
WrapWithValueConverter code can be found in the linked overflow question at the top.

public class SalesOrder
{
  [JsonProperty("project", NullValueHandling = NullValueHandling.Ignore)]
  [JsonConverter(typeof(WrapWithValueConverter<int?>))]
  public int? Project { get; set; }
  
  [JsonProperty("printDescriptionOnInvoice", NullValueHandling = NullValueHandling.Ignore)]
  [JsonConverter(typeof(WrapWithValueConverter<bool>))]
  public bool PrintDescriptionOnInvoice { get; set; }

  [JsonProperty("printNoteOnExternalDocuments", NullValueHandling = NullValueHandling.Ignore)]
  [JsonConverter(typeof(WrapWithValueConverter<bool>))]
  public bool PrintNoteOnExternalDocuments { get; set; }

  [JsonProperty("printNoteOnInternalDocuments", NullValueHandling = NullValueHandling.Ignore)]
  [JsonConverter(typeof(WrapWithValueConverter<bool>))]
  public bool PrintNoteOnInternalDocuments { get; set; }

  [JsonProperty("soBillingContact", NullValueHandling = NullValueHandling.Ignore)]
  [JsonConverter(typeof(ContactDtoJsonConverter))]
  public Contact SoBillingContact { get; set; }
}

ContactDtoJsonConverter Class:

public class ContactDtoJsonConverter : JsonConverter<Contact>
{
    public override bool CanRead => false;

    public override bool CanWrite => true;

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

    public override void WriteJson(JsonWriter writer, Contact value, JsonSerializer serializer)
    {
        var dtoContact = new DtoContact
        {
            Value = value
        };
        JToken t = JToken.FromObject(dtoContact);
        JObject o = (JObject)t;

        o.WriteTo(writer);
    }
}

DtoContact Class:

public class DtoContact
{
  [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
  public Contact Value { get; set; }
}

Contact Class:

public class Contact
{
   [JsonProperty("overrideContact", NullValueHandling = NullValueHandling.Ignore)]
   public bool OverrideContact { get;set; }

   [JsonProperty("attention", NullValueHandling = NullValueHandling.Ignore)]
   [JsonConverter(typeof(StringDtoJsonConverter))]
   public string Attention { get; set; }
  
   [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)]
   [JsonConverter(typeof(StringDtoJsonConverter))]
   public string Email { get; set; }
  
   [JsonProperty("fax", NullValueHandling = NullValueHandling.Ignore)]
   [JsonConverter(typeof(StringDtoJsonConverter))]
   public string Fax { get; set; }
  
   [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
   [JsonConverter(typeof(StringDtoJsonConverter))]
   public string Name { get; set; }
  
   [JsonProperty("phone1", NullValueHandling = NullValueHandling.Ignore)]
   [JsonConverter(typeof(StringDtoJsonConverter))]
   public string Phone1 { get; set; }
  
   [JsonProperty("phone2", NullValueHandling = NullValueHandling.Ignore)]
   [JsonConverter(typeof(StringDtoJsonConverter))]
   public string Phone2 { get; set; }
  
   [JsonProperty("web", NullValueHandling = NullValueHandling.Ignore)]
   [JsonConverter(typeof(StringDtoJsonConverter))]
   public string Web { get; set; }
}

StringDtoJsonConverter Class:

public class StringDtoJsonConverter : JsonConverter<string>
{
  public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer)
  {
    return (string)reader.Value;
  }
  
  public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer)
  {
    JToken t = JToken.FromObject(value);
    if (t.Type != JTokenType.Object)
    {
      var dtoValue = new DtoString
      {
        Value = value
      };
      serializer.Serialize(writer, dtoValue);
    }
  }
}

CodePudding user response:

you can try this code,that doesn't need any custom converters

    var jsonObj = JObject.Parse(json);

    SalesOrder salesOrder = null;
    
    if (jsonObj["printNoteOnInternalDocuments"].Type == JTokenType.Boolean) 
                                                salesOrder = jsonObj.ToObject<SalesOrder>();
else
{
  var newJsonObj = new JObject
  {
  ["printNoteOnInternalDocuments"] = jsonObj["printNoteOnInternalDocuments"]["value"],

  ["soBillingContact"] = new JObject(  ((JObject) jsonObj["soBillingContact"]["value"]).Properties()
                .Select(p=> new JProperty( p.Name,p.Value["value"])))
  };

        salesOrder = newJsonObj.ToObject<SalesOrder>();
}

CodePudding user response:

The converter from this answer to C# Newtonsoft.Json Custom Deserializer can be fixed to avoid the Self referecing loop error by applying [JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)] to DTO.value like so:

sealed class DTO { [JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)] public TValue value { get; set; } public object GetValue() => value; }

Note that, by doing this, PreserveReferencesHandling will be disabled.

That being said, you wrote It should be able to handle these two JSON formats. If you know in advance which format you require, the easiest way to handle both would be to create a custom contract resolver that applies the converter in runtime. Doing so has the added advantage of allowing you to remove all the [JsonConverter(typeof(WrapWithValueConverter<T>))] attributes from your model.

First define the following contract resolver:

public class WrapWithValueContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property.Converter == null && property.ItemConverter == null) // property.Converter check is required to avoid applying the converter to WrapWithValueConverter<TValue>.DTO.value
            property.Converter = (JsonConverter)Activator.CreateInstance(typeof(WrapWithValueConverter<>).MakeGenericType(property.PropertyType));
        return property;
    }
}

public class WrapWithValueConverter<TValue> : JsonConverter
{
    // Here we take advantage of the fact that a converter applied to a property has highest precedence to avoid an infinite recursion.
    sealed class DTO { [JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling  = ReferenceLoopHandling.Serialize)] public TValue value { get; set; } public object GetValue() => value; }

    public override bool CanConvert(Type objectType) => typeof(TValue).IsAssignableFrom(objectType);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        => serializer.Serialize(writer, new DTO { value = (TValue)value });

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        => serializer.Deserialize<DTO>(reader)?.GetValue();
}

public class NoConverter : JsonConverter
{
    // NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
    // By https://stackoverflow.com/users/3744182/dbc
    // To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
    public override bool CanConvert(Type objectType)  { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }
    public override bool CanRead => false;
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}

Now, if you want the wrapped format, serialize and deserialize using the following settings:

DefaultContractResolver resolver = new WrapWithValueContractResolver // Cache statically and reuse for best performance
{
    //NamingStrategy = new CamelCaseNamingStrategy(), // Uncomment if you need camel case
}; 

var json = JsonConvert.SerializeObject(vSalesOrder, Formatting.Indented, settings);

var order2 = JsonConvert.DeserializeObject<SalesOrder>(json, settings);

If you don't want the wrapped format, serialize and deserialize without setting a resolver as usual.

Demo fiddle here.

  • Related