В настоящее время я занимаюсь миграцией vom Newtonsoft на System.Text.Json. Newtonsoft смогла автоматически десериализовать объекты, имеющие одно или несколько свойств интерфейса. С System.Text.Json я получаю следующее сообщение об ошибке для соответствующих классов, когда пытаюсь выполнить то же самое:
each parameter in the deserialization constructor must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object.
Я могу избежать этой проблемы, написав пользовательские преобразователи, но это приведет к большим накладным расходам для объектов с несколькими вложенными слоями, где каждое свойство интерфейса может снова иметь несколько свойств интерфейса (таким образом, требуется несколько пользовательских преобразователей). Есть ли более простое решение этой проблемы?
Я создаю пример, чтобы проиллюстрировать проблему:
[Fact]
public void JsonTest()
{
var child = new Child(new Name("Peter"), 10);
var parent = new Parent(child);
var str = JsonSerializer.Serialize(parent);
var jsonObj = JsonSerializer.Deserialize<Parent>(str);
Console.WriteLine(jsonObj!.Child.Name);
}
}
[JsonConverter(typeof(ParentConverter))]
public class Parent
{
public Parent(Child child)
{
Child = child;
}
public IChild Child { get; set;}
}
[JsonConverter(typeof(ChildConverter))]
public class Child : IChild
{
[JsonConstructor]
public Child(Name name, int age)
{
Name = name;
Age = age;
}
public IName Name { get; set; }
public int Age { get; set; }
}
public class Name : IName
{
public string NameValue { get; set; }
public Name(string nameValue)
{
NameValue = nameValue;
}
}
public interface IChild
{
IName Name { get; }
int Age { get; }
}
public interface IName
{
string NameValue { get; }
}
public class ParentConverter : JsonConverter<Parent>
{
public override Parent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (JsonDocument document = JsonDocument.ParseValue(ref reader))
{
JsonElement root = document.RootElement;
if (root.TryGetProperty("Child", out JsonElement childElement))
{
Child? child = JsonSerializer.Deserialize<Child>(childElement.GetRawText(), options);
return new Parent(child!);
}
}
throw new JsonException("Invalid JSON data");
}
public override void Write(Utf8JsonWriter writer, Parent value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName("Child");
JsonSerializer.Serialize(writer, value.Child, options);
writer.WriteEndObject();
}
}
public class ChildConverter : JsonConverter<Child>
{
public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (JsonDocument document = JsonDocument.ParseValue(ref reader))
{
var name = new Name("Alex");
var age = 20;
JsonElement root = document.RootElement;
if (root.TryGetProperty("Name", out JsonElement nameElement))
{
name = JsonSerializer.Deserialize<Name>(nameElement.GetRawText(), options);
}
if (root.TryGetProperty("Age", out JsonElement ageElement))
{
age = JsonSerializer.Deserialize<int>(ageElement.GetRawText(), options);
}
return new Child(name!, age);
}
throw new JsonException("Invalid JSON data");
}
public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName("Name");
JsonSerializer.Serialize(writer, value.Name, options);
writer.WritePropertyName("Age");
JsonSerializer.Serialize(writer, value.Age, options);
writer.WriteEndObject();
}
}
С этими пользовательскими конвертерами я получаю ожидаемое поведение. Но есть ли способ избежать написания собственного конвертера для каждого такого класса?
Десериализация типов интерфейса не поддерживается в System.Text.Json, так что это не поможет моей проблеме.
Ах. Я понимаю. Это бы исправило выделенное сообщение об ошибке, которое вы показали. Возможно, вы захотите удалить его, так как он отвлекает от реальной проблемы.
Как вы просите что-то проще. Почему бы просто не использовать Json.Net. Насколько я понимаю, даже Microsoft рассматривает System.Text.Json как еще один вариант, а не как замену.
Основными причинами являются согласованность и отсутствие какого-либо нежелательного поведения при использовании нескольких разных библиотек. Мотивация перехода на System.Text.Json в основном связана с повышением производительности. Спасибо за ваш вклад!





Это называется полиморфной сериализацией. Библиотеке нужен какой-то способ аннотировать конкретную реализацию IChild свойства. Я думаю, что Newtonsoft добавляет свойство с полным именем типа, хотя это просто в использовании, у него есть некоторые потенциальные недостатки, если вы хотите переименовать свой класс.
System.Text.Json использует атрибуты для аннотирования производных типов и требует, чтобы вы предоставили идентификаторы. Подробнее о Дискриминаторах полиморфных типов.
[JsonDerivedType(typeof(Child1), typeDiscriminator: "Child1")]
[JsonDerivedType(typeof(Child2), typeDiscriminator: "Child2")]
public interface IChild
{
int Age { get; }
}
public class Child1 : IChild
{
public string PreferedToy { get; set; }
public int Age { get; set; }
}
public class Child2 : IChild
{
public string Name { get; set; }
public int Age { get; set; }
}
Если у вас есть только одна реализация каждого интерфейса, я бы подумал либо об избавлении от интерфейсов, либо о преобразовании ваших объектов в объекты передачи данных (DTO), которые максимально просты для сериализации. Использование DTO помогает отделить проблемы сериализации от любой логики в ваших классах.
Прежде всего, спасибо за ваш быстрый ответ. К сожалению, похоже, что эта функция недоступна для .NET 6. Я рассмотрю реструктуризацию кода (например, удаление ненужных интерфейсов), но если это невозможно, я думаю, мне придется создать обходной путь, аналогичный тому, который я опубликовано.
@Steve Это должно быть доступно как минимум в System.Text.Json 7.0.2, так как я тестировал его там. И эта версия должна быть полностью совместима с .net 4.6.2. Однако в документации это не очень ясно.
Ах, хорошо, я, к сожалению, все еще получаю то же исключение при аннотации интерфейса IChild с помощью: [JsonDerivedType(typeof(Child), typeDiscriminator: "Child")] и интерфейса IName с помощью: [JsonDerivedType(typeof(Name), typeDiscriminator: "Name ")]
Хорошо, при изменении типов параметров конструктора с Child на IChild и Name на IName это работает. Большое спасибо за Вашу помощь!
При написании этого примера я забыл учесть одну вещь: в моем реальном приложении я получаю json из http-запроса и не сериализую его самостоятельно. Затем json не обогащается необходимыми метаданными для правильной десериализации. Любое решение для этого?
@ Стив, я не уверен. Если у вас нет информации о типе, вы не можете поддерживать полиморфизм. Я бы просто использовал шаблон dto для таких случаев.
Ваши конструкторы берут конкретные типы, но затем отображают их как интерфейс. Поэтому вам следует попробовать использовать интерфейс также в конструкторе (передача IChild, а не Child и IName, а не Name), чтобы сигнатура конструктора соответствовала поверхности класса.