5

I need to convert my class to JSON and I use Json.NET. But I can have different JSON structures, like:

{
    name: "Name",
    type: "simple1",
    value: 100
};

or

{
    name: "Name",
    type: {
        optional1: {
            setting1: "s1",
            setting2: "s2",
            ///etc.
    },
    value: 100
};

My C# code is:

public class Configuration
{
    [JsonProperty(PropertyName = "name")]
    public string Name{ get; set; }

    [JsonProperty(PropertyName = "type")]
    public MyEnumTypes Type { get; set; }

    public OptionalType TypeAdditionalData { get; set; }

    [JsonProperty(PropertyName = "value")]
    public int Value { get; set; }
    public bool ShouldSerializeType()
    {
        OptionalSettingsAttribute optionalSettingsAttr = this.Type.GetAttributeOfType<OptionalSettingsAttribute>();
        return optionalSettingsAttr == null;
    }

    public bool ShouldSerializeTypeAdditionalData()
    {
        OptionalSettingsAttribute optionalSettingsAttr = this.Type.GetAttributeOfType<OptionalSettingsAttribute>();
        return optionalSettingsAttr != null;
    }
}

public enum MyEnumTypes 
{
    [EnumMember(Value = "simple1")]
    Simple1,

    [EnumMember(Value = "simple2")]
    Simple2,

    [OptionalSettingsAttribute]
    [EnumMember(Value = "optional1")]
    Optional1,

    [EnumMember(Value = "optional2")]
    [OptionalSettingsAttribute]
    Optional2
}

My idea was when Configuration.Type - value hasn't attribute OptionalSettingsAttribute - to serialize it as type: "simple1". Otherwise - to use Configuration.Type - value as type's value key (type: { optional1: {} }) and value in Configuration.TypeAdditionalData as optional1 - value (like 2 simple JSON above).

I tried to create a custom Converter, like:

public class ConfigurationCustomConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(Configuration).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return serializer.Deserialize<Configuration>(reader);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        //my changes here

        serializer.Serialize(writer, value);
    }

But when I add [JsonConverter(typeof(ConfigurationCustomConverter))] attribute to Configuration class:

[JsonConverter(typeof(ConfigurationCustomConverter))]
public class Configuration

and called JsonConvert.SerializeObject(configurationObj); I received next error:

Self referencing loop detected with type 'Configuration'. Path ''.

Do you have any ideas how to change my code to serialize my class to 2 different JSON structures? Note: I won't use the same class to deserialize the JSON.

Thank you!

Pepo
  • 135
  • 1
  • 10

2 Answers2

4

The reason you are getting the Self referencing loop detected exception is that the WriteJson method of your converter is calling itself recursively. When you apply a converter to a type using [JsonConverter(typeof(ConfigurationCustomConverter))], the WriteJson() method will unconditionally replace Json.NET's default implementation. Thus your inner call:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    //my changes here
    serializer.Serialize(writer, value);
}

would cause a stack overflow. Json.NET notices this and instead throws the exception you see. For more details, see JSON.Net throws StackOverflowException when using [JsonConvert()]. Setting ReferenceLoopHandling.Ignore simply causes the infinite recursion to be skipped, leaving your object empty.

You have a few options to solve this problem:

  1. You could manually write all property names and values other than Type and TypeAdditionalData then write out the custom "type" property last. For instance:

    [JsonConverter(typeof(ConfigurationConverter))]
    public class Configuration
    {
        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }
    
        public MyEnumTypes Type { get; set; }
    
        public OptionalType TypeAdditionalData { get; set; }
    
        [JsonProperty(PropertyName = "value")]
        public int Value { get; set; }
    }
    
    class ConfigurationConverter : JsonConverter
    {
        const string typeName = "type";
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(Configuration).IsAssignableFrom(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
                return null;
            var config = (existingValue as Configuration ?? (Configuration)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
    
            // Populate the regular property values.
            var obj = JObject.Load(reader);
            var type = obj.RemoveProperty(typeName);
            using (var subReader = obj.CreateReader())
                serializer.Populate(subReader, config);
    
            // Populate Type and OptionalType
            if (type is JValue) // Primitive value
            {
                config.Type = type.ToObject<MyEnumTypes>(serializer);
            }
            else
            {
                var dictionary = type.ToObject<Dictionary<MyEnumTypes, OptionalType>>(serializer);
                if (dictionary.Count > 0)
                {
                    config.Type = dictionary.Keys.First();
                    config.TypeAdditionalData = dictionary.Values.First();
                }
            }
    
            return config;
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var config = (Configuration)value;
            var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(config.GetType());
            writer.WriteStartObject();
            foreach (var property in contract.Properties
                .Where(p => p.Writable && (p.ShouldSerialize == null || p.ShouldSerialize(config)) && !p.Ignored))
            {
                if (property.UnderlyingName == "Type" || property.UnderlyingName == "TypeAdditionalData")
                    continue;
                var propertyValue = property.ValueProvider.GetValue(config);
                if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore)
                    continue;
                writer.WritePropertyName(property.PropertyName);
                serializer.Serialize(writer, propertyValue);
            }
            writer.WritePropertyName(typeName);
            if (config.Type.GetCustomAttributeOfEnum<OptionalSettingsAttribute>() == null)
            {
                serializer.Serialize(writer, config.Type);
            }
            else
            {
                var dictionary = new Dictionary<MyEnumTypes, OptionalType>
                {
                    { config.Type, config.TypeAdditionalData },
                };
                serializer.Serialize(writer, dictionary);
            }
            writer.WriteEndObject();
        }
    }
    
    public class OptionalType
    {
        public string setting1 { get; set; }
    }
    
    public class OptionalSettingsAttribute : System.Attribute
    {
        public OptionalSettingsAttribute()
        {
        }
    }
    
    [JsonConverter(typeof(StringEnumConverter))]
    public enum MyEnumTypes
    {
        [EnumMember(Value = "simple1")]
        Simple1,
    
        [EnumMember(Value = "simple2")]
        Simple2,
    
        [OptionalSettingsAttribute]
        [EnumMember(Value = "optional1")]
        Optional1,
    
        [EnumMember(Value = "optional2")]
        [OptionalSettingsAttribute]
        Optional2
    }
    
    public static class EnumExtensions
    {
        public static TAttribute GetCustomAttributeOfEnum<TAttribute>(this Enum value)
            where TAttribute : System.Attribute
        {
            var type = value.GetType();
            var memInfo = type.GetMember(value.ToString());
            return memInfo[0].GetCustomAttribute<TAttribute>();
        }
    }
    
    public static class JsonExtensions
    {
        public static JToken RemoveProperty(this JObject obj, string name)
        {
            if (obj == null)
                return null;
            var property = obj.Property(name);
            if (property == null)
                return null;
            var value = property.Value;
            property.Remove();
            property.Value = null;
            return value;
        }
    }
    

    Notice I added [JsonConverter(typeof(StringEnumConverter))] to your enum. This ensures the type is always written as a string.

    Sample fiddle.

  2. You could disable recursive calls to the converter via the technique shown in JSON.Net throws StackOverflowException when using [JsonConvert()], generate a default serialization, modify it as required, and write it out.

  3. You could avoid the use of a converter entirely by marking Type and TypeAdditionalData as [JsonIgnore] and introducing an additional private property to serialize and deserialize "type":

    public class Configuration
    {
        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }
    
        [JsonIgnore]
        public MyEnumTypes Type { get; set; }
    
        [JsonIgnore]
        public OptionalType TypeAdditionalData { get; set; }
    
        [JsonProperty("type")]
        JToken SerializedType
        {
            get
            {
                if (Type.GetCustomAttributeOfEnum<OptionalSettingsAttribute>() == null)
                {
                    return JToken.FromObject(Type);
                }
                else
                {
                    var dictionary = new Dictionary<MyEnumTypes, OptionalType>
                    {
                        { Type, TypeAdditionalData },
                    };
                    return JToken.FromObject(dictionary);
                }
            }
            set
            {
                if (value == null || value.Type == JTokenType.Null)
                {
                    TypeAdditionalData = null;
                    Type = default(MyEnumTypes);
                }
                else if (value is JValue)
                {
                    Type = value.ToObject<MyEnumTypes>();
                }
                else
                {
                    var dictionary = value.ToObject<Dictionary<MyEnumTypes, OptionalType>>();
                    if (dictionary.Count > 0)
                    {
                        Type = dictionary.Keys.First();
                        TypeAdditionalData = dictionary.Values.First();
                    }
                }
            }
        }
    
        [JsonProperty(PropertyName = "value")]
        public int Value { get; set; }
    }
    
dbc
  • 104,963
  • 20
  • 228
  • 340
1

If you need to move past that error, you can configure your serialization to ignore the reference loop. This is done by using one of the SerializaObject() overloads.

JsonConvert.SerializeObject(configurationObj,
                    new JsonSerializerSettings()
                    { 
                        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                    });
Matt Rowland
  • 4,575
  • 4
  • 25
  • 34
  • Thank you! This fixed the "Reference loop" error, but if `WriteJson` method has next code: `public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); }` the serialized string is empty - nothing has converted. Is it correct? How I can use the default converter in my custom converter? Thank you! – Pepo Jun 18 '16 at 14:28
  • I have not run into the situation that you are describing and I am not next to my computer to investigate more. I will be on Monday. Hopefully someone else can help before then. Please update your question with progress you made with the information that I provided so the next person can help answer the full question. – Matt Rowland Jun 18 '16 at 14:32
  • 1
    Thank you! I used the solution #3 in @dbc - answer and it is working for me! – Pepo Jun 20 '16 at 07:01