0

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);
    }
  }
}
Snuffsis
  • 21
  • 5
  • 1
    Please share with us your custom converter's code as well – Peter Csala Nov 30 '22 at 08:23
  • And the class definition too. As direct translation to thing like `public class Web{public string Value { get; set; }}`, you may want to exclude them from possible answers. – Drag and Drop Nov 30 '22 at 08:34
  • @PeterCsala The custom converter code that I am trying is exactly the same as the one in the previously linked stack overflow question. The custom converter code that I made that worked looks like this: ```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); } }``` – Snuffsis Nov 30 '22 at 09:02
  • Could you please share with us the definition of the `Contact` class? – Peter Csala Nov 30 '22 at 09:08
  • I added the class definition along with the whole custom converter code to the question itself. – Snuffsis Nov 30 '22 at 09:09
  • Could you please share with us the definition of the `StringDtoJsonConverter` class? – Peter Csala Nov 30 '22 at 09:26
  • Added it. But that converter could have been replaced by the ValueJsonConverter that the suggested code in the other mentioned question has, since it does essentially the same thing, except this is specifically for strings. It's also not required to replicate the issue. – Snuffsis Nov 30 '22 at 09:40
  • WrapWithValueConverter I meant. – Snuffsis Nov 30 '22 at 09:51
  • Could you please share with use the usage of the `ContactDtoJsonConverter`? – Peter Csala Nov 30 '22 at 10:12
  • I have now added all the code that was missing, including Main so you can see how the object is created and called upon when serializing it. – Snuffsis Nov 30 '22 at 10:56
  • We are almost there. It does not compile because `SalesOrder`'s ctor is referring to a field which is not defined `OrderType`. – Peter Csala Nov 30 '22 at 11:24
  • 1
    Ah of course. You can just remove that constructor. Its not necessary – Snuffsis Nov 30 '22 at 11:37
  • There is a lot of text, but absolutely unclear what are trying to reach. Can you just post 2 jsons - origional and one you are trying to convert to. I don't think that you need any custom converters at all. It doesn't make any sense to post tons of code that is not working. – Serge Nov 30 '22 at 12:19
  • The code provided is what I have now that is currently working fine. What isn't working is when trying to use the solution in the linked question to make just one converter that can handle datatypes as well as various classes, like SalesOrder in my case. In which case it throws an exception for a self referencing loop when it encounters a nested object. And what the converter should do is required. Because the API uses two different styles depending on endpoint. So it needs to be able to convert between regular json format, and this value format that they have in my example – Snuffsis Nov 30 '22 at 12:46
  • @Snuffsis Do you think it looks good when you created hundreds line of code to deserialize 3 lines of json? – Serge Nov 30 '22 at 12:56
  • That is part of why I want to make it simpler and have a generic converter that can handle the various different properties and objects. – Snuffsis Nov 30 '22 at 13:48
  • *It should be able to handle these two JSON formats* -- just to be clear, you don't know whether you are going to receive one format or the other, and need to handle either one in via the same converter? Or do you know in advance, and you need to toggle the conversion on and off? – dbc Nov 30 '22 at 19:21

2 Answers2

2

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
0

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>();
}
Serge
  • 40,935
  • 4
  • 18
  • 45
  • That is a solution that would be specifically for just the example. I made a repository with examples of the code above, with both working (with specific converters) and non-working (generic converter) examples. https://github.com/Snuffsis/ConverterExample Hopefully that helps with understanding what the problem is. – Snuffsis Nov 30 '22 at 14:41
  • @Snuffsis You have classes, so you should know what json you have to deserialize. So you will have to customize your code for each case too. My post is just a hint. It can be easily made generic, I am just not a free coder to do this for you. – Serge Nov 30 '22 at 14:53
  • I should have been clearer I guess. The issue I needed help with wasn't really the conversion itself. It was for the exception I was getting. – Snuffsis Nov 30 '22 at 16:21