Json.NET different json structure, based on enum value

爱⌒轻易说出口 提交于 2019-12-10 15:47:40

问题


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!


回答1:


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; }
    }
    



回答2:


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
                    });


来源:https://stackoverflow.com/questions/37896661/json-net-different-json-structure-based-on-enum-value

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!