Подтвердить что ты не робот

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

}
4b9b3361

Ответ 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;
    }
}

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