Json.NET, как настроить сериализацию для вставки свойства JSON

Мне не удалось найти разумную реализацию для JsonConvert.WriteJson, которая позволяла бы мне вставлять свойство JSON при сериализации определенных типов. Все мои попытки привели к "JsonSerializationException: обнаружен цикл самоссылки с типом XXX".

Немного больше информации о проблеме, которую я пытаюсь решить: я использую JSON в качестве формата файла конфигурации, и я использую JsonConverter для управления разрешением типов, сериализацией и десериализацией моих типов конфигурации. Вместо использования свойства $type я хочу использовать более значимые значения JSON, которые используются для разрешения правильных типов.

В моем урезанном примере вот текст JSON:

{
  "Target": "B",
  "Id": "foo"
}

где свойство JSON "Target": "B" используется для определения того, что этот объект должен быть сериализован в тип B. Этот дизайн может показаться неубедительным, учитывая простой пример, но он делает формат файла конфигурации более удобным для использования.

Я также хочу, чтобы файлы конфигурации были переключаемыми. У меня работает дело десериализации, но я не могу получить дело от сериализации.

Корень моей проблемы в том, что я не могу найти реализацию JsonConverter.WriteJson, которая использует стандартную логику сериализации JSON и не выдает исключение "цикл самоссылки". Вот моя реализация:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

    //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
    // Same error occurs whether I use the serializer parameter or a separate serializer.
    JObject jo = JObject.FromObject(value, serializer); 
    if (typeHintProperty != null)
    {
        jo.AddFirst(typeHintProperty);
    }
    writer.WriteToken(jo.CreateReader());
}

Мне кажется, это ошибка в Json.NET, потому что должен быть способ сделать это. К сожалению, все примеры JsonConverter.WriteJson, с которыми я сталкивался (например, пользовательское преобразование определенных объектов в JSON.NET), обеспечивают только пользовательскую сериализацию определенного класса, используя методы JsonWriter для записи отдельных объектов. и свойства.

Вот полный код для теста xunit, который показывает мою проблему (или смотрите здесь)

using System;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

using Xunit;


public class A
{
    public string Id { get; set; }
    public A Child { get; set; }
}

public class B : A {}

public class C : A {}

/// <summary>
/// Shows the problem I'm having serializing classes with Json.
/// </summary>
public sealed class JsonTypeConverterProblem
{
    [Fact]
    public void ShowSerializationBug()
    {
        A a = new B()
              {
                  Id = "foo",
                  Child = new C() { Id = "bar" }
              };

        JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
        jsonSettings.ContractResolver = new TypeHintContractResolver();
        string json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings);
        Console.WriteLine(json);

        Assert.Contains(@"""Target"": ""B""", json);
        Assert.Contains(@"""Is"": ""C""", json);
    }

    [Fact]
    public void DeserializationWorks()
    {
        string json =
@"{
  ""Target"": ""B"",
  ""Id"": ""foo"",
  ""Child"": { 
        ""Is"": ""C"",
        ""Id"": ""bar"",
    }
}";

        JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
        jsonSettings.ContractResolver = new TypeHintContractResolver();
        A a = JsonConvert.DeserializeObject<A>(json, jsonSettings);

        Assert.IsType<B>(a);
        Assert.IsType<C>(a.Child);
    }
}

public class TypeHintContractResolver : DefaultContractResolver
{
    public override JsonContract ResolveContract(Type type)
    {
        JsonContract contract = base.ResolveContract(type);
        if ((contract is JsonObjectContract)
            && ((type == typeof(A)) || (type == typeof(B))) ) // In the real implementation, this is checking against a registry of types
        {
            contract.Converter = new TypeHintJsonConverter(type);
        }
        return contract;
    }
}


public class TypeHintJsonConverter : JsonConverter
{
    private readonly Type _declaredType;

    public TypeHintJsonConverter(Type declaredType)
    {
        _declaredType = declaredType;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == _declaredType;
    }

    // The real implementation of the next 2 methods uses reflection on concrete types to determine the declaredType hint.
    // TypeFromTypeHint and TypeHintPropertyForType are the inverse of each other.

    private Type TypeFromTypeHint(JObject jo)
    {
        if (new JValue("B").Equals(jo["Target"]))
        {
            return typeof(B);
        }
        else if (new JValue("A").Equals(jo["Hint"]))
        {
            return typeof(A);
        }
        else if (new JValue("C").Equals(jo["Is"]))
        {
            return typeof(C);
        }
        else
        {
            throw new ArgumentException("Type not recognized from JSON");
        }
    }

    private JProperty TypeHintPropertyForType(Type type)
    {
        if (type == typeof(A))
        {
            return new JProperty("Hint", "A");
        }
        else if (type == typeof(B))
        {
            return new JProperty("Target", "B");
        }
        else if (type == typeof(C))
        {
            return new JProperty("Is", "C");
        }
        else
        {
            return null;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (! CanConvert(objectType))
        {
            throw new InvalidOperationException("Can't convert declaredType " + objectType + "; expected " + _declaredType);
        }

        // Load JObject from stream.  Turns out we're also called for null arrays of our objects,
        // so handle a null by returning one.
        var jToken = JToken.Load(reader);
        if (jToken.Type == JTokenType.Null)
            return null;
        if (jToken.Type != JTokenType.Object)
        {
            throw new InvalidOperationException("Json: expected " + _declaredType + "; got " + jToken.Type);
        }
        JObject jObject = (JObject) jToken;

        // Select the declaredType based on TypeHint
        Type deserializingType = TypeFromTypeHint(jObject);

        var target = Activator.CreateInstance(deserializingType);
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

        //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
        // Same error occurs whether I use the serializer parameter or a separate serializer.
        JObject jo = JObject.FromObject(value, serializer); 
        if (typeHintProperty != null)
        {
            jo.AddFirst(typeHintProperty);
        }
        writer.WriteToken(jo.CreateReader());
    }

}

Ответ 1

Вызов JObject.FromObject() из конвертера на том же преобразованном объекте приведет к рекурсивному циклу, как вы видели. Обычно решение заключается в том, чтобы либо (a) использовать отдельный экземпляр JsonSerializer внутри конвертера, либо (b) сериализовать свойства вручную, как указал Джеймс в его ответе. Ваш случай немного особенный в том, что ни одно из этих решений действительно не работает для вас: если вы используете отдельный экземпляр serializer, который не знает о конверторе, тогда ваши дочерние объекты не будут использовать свои свойства намека. И сериализация полностью вручную не работает для обобщенного решения, как вы упомянули в своих комментариях.

К счастью, есть средняя почва. Вы можете использовать немного отражения в вашем методе WriteJson для получения свойств объекта, а затем делегировать оттуда до JToken.FromObject(). Конвертер будет называться рекурсивно, как и для дочерних свойств, но не для текущего объекта, так что вы не попадаете в неприятности. Одно предостережение с этим решением: если у вас есть какие-либо атрибуты [JsonProperty], применяемые к классам, обрабатываемым этим конвертером (A, B и C в вашем примере), эти атрибуты не будут соблюдаться.

Вот обновленный код для метода WriteJson:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

    JObject jo = new JObject();
    if (typeHintProperty != null)
    {
        jo.Add(typeHintProperty);
    }
    foreach (PropertyInfo prop in value.GetType().GetProperties())
    {
        if (prop.CanRead)
        {
            object propValue = prop.GetValue(value);
            if (propValue != null)
            {
                jo.Add(prop.Name, JToken.FromObject(propValue, serializer));
            }
        }
    }
    jo.WriteTo(writer);
}

Fiddle: https://dotnetfiddle.net/jQrxb8

Ответ 2

Пример использования пользовательского конвертера для получения свойства, которое мы игнорируем, разбивает его и добавляет его свойства к родительскому объекту.

public class ContextBaseSerializer : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(ContextBase).GetTypeInfo().IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var contextBase = value as ContextBase;
        var valueToken = JToken.FromObject(value, new ForcedObjectSerializer());

        if (contextBase.Properties != null)
        {
            var propertiesToken = JToken.FromObject(contextBase.Properties);
            foreach (var property in propertiesToken.Children<JProperty>())
            {
                valueToken[property.Name] = property.Value;
            }
        }

        valueToken.WriteTo(writer);
    }
}

Мы должны переопределить сериализатор, чтобы мы могли указать собственный распознаватель:

public class ForcedObjectSerializer : JsonSerializer
{
    public ForcedObjectSerializer()
        : base()
    {
        this.ContractResolver = new ForcedObjectResolver();
    }
}

А в настраиваемом преобразователе мы удалим конвертер из JsonContract, это заставит внутренние сериализаторы использовать сериализатор объектов по умолчанию:

public class ForcedObjectResolver : DefaultContractResolver
{
    public override JsonContract ResolveContract(Type type)
    {
        // We're going to null the converter to force it to serialize this as a plain object.
        var contract =  base.ResolveContract(type);
        contract.Converter = null;
        return contract;
    }
}

Это должно привести вас туда или достаточно близко. :) Я использую это в https://github.com/RoushTech/SegmentDotNet/, в котором есть тестовые случаи, охватывающие этот вариант использования (включая вложение нашего настраиваемого сериализованного класса), подробности обсуждения обсуждаемые здесь: https://github.com/JamesNK/Newtonsoft.Json/issues/386

Ответ 3

Как насчет этого:

public class TypeHintContractResolver : DefaultContractResolver
{

  protected override IList<JsonProperty> CreateProperties(Type type,
      MemberSerialization memberSerialization)
  {
    IList<JsonProperty> result = base.CreateProperties(type, memberSerialization);
    if (type == typeof(A))
    {
      result.Add(CreateTypeHintProperty(type,"Hint", "A"));
    }
    else if (type == typeof(B))
    {
      result.Add(CreateTypeHintProperty(type,"Target", "B"));
    }
    else if (type == typeof(C))
    {
      result.Add(CreateTypeHintProperty(type,"Is", "C"));
    }
    return result;
  }

  private JsonProperty CreateTypeHintProperty(Type declaringType, string propertyName, string propertyValue)
  {
    return new JsonProperty
    {
        PropertyType = typeof (string),
        DeclaringType = declaringType,
        PropertyName = propertyName,
        ValueProvider = new TypeHintValueProvider(propertyValue),
        Readable = false,
        Writable = true
    };
  }
}

Требуемый для этого тип поставщика значений может быть таким же простым, как это:

public class TypeHintValueProvider : IValueProvider
{

  private readonly string _value;
  public TypeHintValueProvider(string value)
  {
    _value = value;
  }

  public void SetValue(object target, object value)
  {        
  }

  public object GetValue(object target)
  {
    return _value;
  }

}

Fiddle: https://dotnetfiddle.net/DRNzz8

Ответ 4

Сериализатор звонит в ваш конвертер, который затем вызывается в сериализатор, который звонит в ваш конвертер и т.д.

Либо используйте новый экземпляр сериализатора, у которого нет конвертера с JObject.FromObject, или сериализуйте члены типа вручную.

Ответ 5

У меня была аналогичная проблема, и вот что я делаю в разрешении контракта

if (contract is JsonObjectContract && ShouldUseConverter(type))     
{
    if (contract.Converter is TypeHintJsonConverter)
    {
        contract.Converter = null;
    }
    else
    {
        contract.Converter = new TypeHintJsonConverter(type);
    }
}

Это был единственный способ избежать исключения StackOverflowException. Эффективно каждый другой вызов не будет использовать преобразователь.

Ответ 6

Ответ Брайана велик и должен помочь OP, но в ответе есть несколько проблем, с которыми могут столкнуться другие, а именно: 1) исключение переполнения выбрасывается при сериализации свойств массива, 2) любые статические публичные свойства будут испускаться к JSON, которого вы, вероятно, не хотите.

Вот еще одна версия, которая решает эти проблемы:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    Type valueType = value.GetType();
    if (valueType.IsArray)
    {
        var jArray = new JArray();
        foreach (var item in (IEnumerable)value)
            jArray.Add(JToken.FromObject(item, serializer));

        jArray.WriteTo(writer);
    }
    else
    {
        JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

        var jObj = new JObject();
        if (typeHintProperty != null)
            jo.Add(typeHintProperty);

        foreach (PropertyInfo property in valueType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (property.CanRead)
            {
                object propertyValue = property.GetValue(value);
                if (propertyValue != null)
                    jObj.Add(property.Name, JToken.FromObject(propertyValue, serializer));
            }
        }

        jObj.WriteTo(writer);
    }
}

Ответ 7

Столкнулся с этой проблемой в 2019 году :)

Ответ: если вы не хотите, чтобы @stackoverflow не забывал переопределять:

  • bool CanWrite
  • bool CanRead

    public class DefaultJsonConverter : JsonConverter
    {
        [ThreadStatic]
        private static bool _isReading;
    
        [ThreadStatic]
        private static bool _isWriting;
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            try
            {
                _isWriting = true;
    
                Property typeHintProperty = TypeHintPropertyForType(value.GetType());
    
                var jObject = JObject.FromObject(value, serializer);
                if (typeHintProperty != null)
                {
                    jObject.AddFirst(typeHintProperty);
                }
                writer.WriteToken(jObject.CreateReader());
            }
            finally
            {
                _isWriting = false;
            }
        }
    
        public override bool CanWrite
        {
            get
            {
                if (!_isWriting)
                    return true;
    
                _isWriting = false;
    
                return false;
            }
        }
    
        public override bool CanRead
        {
            get
            {
                if (!_isReading)
                    return true;
    
                _isReading = false;
    
                return false;
            }
        }
    
        public override bool CanConvert(Type objectType)
        {
            return true;
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            try
            {
                _isReading = true;
                return serializer.Deserialize(reader, objectType);
            }
            finally
            {
                _isReading = false;
            }
        }
    }
    

Кредит для: https://github.com/RicoSuter/NJsonSchema/blob/master/src/NJsonSchema/Converters/JsonInheritanceConverter.cs

Ответ 8

После того, как вы столкнулись с той же проблемой и, найдя этот и другие подобные вопросы, я обнаружил, что JsonConverter имеет избыточное свойство CanWrite.

Переопределение этого свойства для возврата false исправил эту проблему для меня.

public override bool CanWrite
{
    get
    { 
        return false;
    }
}

Надеюсь, это поможет другим, имеющим ту же проблему.