0

I am having trouble with the requirement to serialize an object in a specific way whereby the object id value becomes the key and the rest of the object forms the value.

Simplified class to be serialized:

[JsonConverter(typeof(FieldTypeConvertor))]
public class FieldType {
    public string Id { get; set; }
    public string Condition  { get; set; }
    public string FieldType { get; set; }
    public string Label { get; set; }
    public string Options { get; set; }
}

Here is my JsonConvertor WriteJson method:

public override void WriteJson(JsonWriter writer, UmbracoFormFieldDto value, JsonSerializer serializer)
{
    var props = value.GetType().GetProperties();
    var idProp = props.FirstOrDefault(p => p.Name.Equals("id", StringComparison.OrdinalIgnoreCase));

    var key = idProp.GetValue(value, null).ToString();

    var newObj = JsonConvert.SerializeObject(value, new JsonSerializerSettings()
    { ContractResolver = new IgnorePropertiesResolver(new[] { "id" }) });

    var container = new JObject { { key, newObj } };
    container.WriteTo(writer);
}

I get why I end up with a StackOverflow but do not know how to avoid it in order generate the output I need which is the following:

"idValueFromOriginalObj": {
    "condition": "propValue",
    "fieldype": "propValue",
    "label": "propValue",
    "options": "propValue"
}

Essentially, the value of id in the original object becomes the key in the serialized object and the rest of the original object properties form the value.

ProNotion
  • 3,662
  • 3
  • 21
  • 30
  • 1
    Take a look at [JSON.Net throws StackOverflowException when using `[JsonConvert()]`](https://stackoverflow.com/q/29719509/3744182), it shows ways to avoid the stack overflow exception when doing a nested serialization inside a converter. – dbc Mar 18 '22 at 16:01
  • That being said, it seems you're using a custom contract resolver `IgnorePropertiesResolver` for your nested serialization. Inside `CreateObjectContract()` you could just set `Converter` to `null` when it's type is `typeof(FieldTypeConvertor)`. That might even be simpler than any of the workarounds in the linked question. – dbc Mar 18 '22 at 16:03
  • Looking at this, It is probably looking through all of the properties. Maybe look at https://www.newtonsoft.com/json/help/html/MaxDepth.htm initially. Also my approach would be 2 DTO's that inherit from a base class then just have the ignore attribute on that specific element you want to ignore. You can switch out the class based on if the id exists. Unless I am misunderstanding the issue. If I remember correctly you can also change a setting on the serializer to ignore nulls which may help you solve this issue. – Netferret Mar 18 '22 at 16:12

1 Answers1

1

Your problem is that, inside JsonConverter.ReadJson(), you are attempting to recursively serialize your value object, but since the converter is applied directly to the type using [JsonConverter(typeof(TConverter))], you are getting a stack overflow.

There are several options to disable a converter for recursive serialization; JSON.Net throws StackOverflowException when using [JsonConvert()] details some of them. However, since you are already using a custom contract resolver IgnorePropertiesResolver to ignore properties named "id", you might enhance the resolver to also ignore converters of type FieldTypeConvertor. The following should do the trick:

public class IgnorePropertiesResolver : DefaultContractResolver
{
    readonly HashSet<string> propertiesToIgnore;
    readonly HashSet<Type> converterTypesToIgnore;

    public IgnorePropertiesResolver(IEnumerable<string> propertiesToIgnore, IEnumerable<Type> converterTypesToIgnore) : base() =>
        (this.propertiesToIgnore, this.converterTypesToIgnore) = 
            ((propertiesToIgnore ?? throw new ArgumentNullException()).ToHashSet(StringComparer.OrdinalIgnoreCase), 
            (converterTypesToIgnore ?? throw new ArgumentNullException()).ToHashSet());
            
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (propertiesToIgnore.Contains(member.Name))
            property.Ignored = true;
        if (property.Converter != null && converterTypesToIgnore.Contains(property.Converter.GetType()))
            property.Converter = null;
        return property;
    }

    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        if (contract.Converter != null && converterTypesToIgnore.Contains(contract.Converter.GetType()))
            contract.Converter = null;
        return contract;
    }
};

Then modify FieldTypeConvertor as follows:

public sealed class FieldTypeConvertor : JsonConverter<UmbracoFormFieldDto>
{
    static readonly IContractResolver innerResolver = new IgnorePropertiesResolver(new [] { "id" }, new [] { typeof(FieldTypeConvertor) })
    {
        NamingStrategy = new CamelCaseNamingStrategy(),
    };

    public override void WriteJson(JsonWriter writer, UmbracoFormFieldDto value, JsonSerializer serializer)
    {
        var props = value.GetType().GetProperties();
        var idProp = props.FirstOrDefault(p => p.Name.Equals("id", StringComparison.OrdinalIgnoreCase));
        var key = idProp.GetValue(value, null).ToString();

        writer.WriteStartObject();
        writer.WritePropertyName(key);
        JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = innerResolver }).Serialize(writer, value);
        writer.WriteEndObject();
    }

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

And your model will be serialized as required:

{
  "idValueFromOriginalObj": {
    "condition": "propValue",
    "fieldType": "propValue",
    "label": "propValue",
    "options": "propValue"
  }
}

Notes:

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thank you for the detailed response which does exactly what I was after, I appreciate the extra information too and links to further reading. – ProNotion Mar 21 '22 at 08:02