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;
}
}
Надеюсь, это поможет другим, имеющим ту же проблему.