'Custom JsonConvertor that also serializes it's value minus one of it's properties

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.



Solution 1:[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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 dbc