Сериализация / десериализация иерархии классов с помощью .NET Core System.Text.Json

У меня есть простая иерархия классов, которую я хочу сериализовать с помощью System.Text.Json.

Всего 3 класса. База Shape. Унаследованные - Box и Circle.

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

Я написал преобразователь типов, который поддерживает сериализацию / десериализацию этой иерархии.

Я пытаюсь понять - это лучший подход для реализации такой функциональности или нет. Действительно, выходной результат сериализации довольно уродлив (я помещаю комментарий в пример ниже). Я не уверен, что это сделано наилучшим образом, в любом случае, просто работает.

Вот пример того, как я реализовал сериализацию / десериализацию:

using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Serialization.Theory
{
    public abstract class Shape
    {
        public abstract String Tag { get; }
    }

    public class Box : Shape
    {
        public override String Tag { get; } = nameof(Box);

        public Single Width { get; set; }

        public Single Height { get; set; }

        public override String ToString()
        {
            return $"{Tag}: Width={Width}, Height={Height}";
        }
    }

    public class Circle : Shape
    {
        public override String Tag { get; } = nameof(Circle);

        public Single Radius { get; set; }

        public override String ToString()
        {
            return $"{Tag}: Radius={Radius}";
        }
    }

    public class ShapeConverter : JsonConverter<Shape>
    {
        public override Boolean CanConvert(Type typeToConvert)
        {
            return typeToConvert == typeof(Circle) || typeToConvert == typeof(Shape);
        }

        public override Shape Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var raw = reader.GetString();
            var doc = JsonDocument.Parse(raw);
            var prop = doc.RootElement.EnumerateObject().Where(x => x.Name == "Tag").First();
            var value = prop.Value.GetString();

            switch (value)
            {
                case nameof(Circle): 
                    return JsonSerializer.Deserialize<Circle>(raw);
                case nameof(Box):
                    return JsonSerializer.Deserialize<Box>(raw);
                default:
                    throw new NotSupportedException();
            }
        }

        public override void Write(Utf8JsonWriter writer, Shape value, JsonSerializerOptions options)
        {
            if (value is Circle circle)
            {
                writer.WriteStringValue(JsonSerializer.SerializeToUtf8Bytes(circle));
            }
            else if (value is Box box)
            {
                writer.WriteStringValue(JsonSerializer.SerializeToUtf8Bytes(box));
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Keep in base class references like it's a property on another object.
            Shape origin1 = new Box { Width = 10, Height = 20 };
            Shape origin2 = new Circle { Radius = 30 };

            var settings = new JsonSerializerOptions();
            settings.Converters.Add(new ShapeConverter());

            var raw1 = JsonSerializer.Serialize(origin1, settings);
            var raw2 = JsonSerializer.Serialize(origin2, settings);

            Console.WriteLine(raw1); // "{\u0022Tag\u0022:\u0022Box\u0022,\u0022Width\u0022:10,\u0022Height\u0022:20}"
            Console.WriteLine(raw2); // "{\u0022Tag\u0022:\u0022Circle\u0022,\u0022Radius\u0022:30}"

            var restored1 = JsonSerializer.Deserialize<Shape>(raw1, settings);
            var restored2 = JsonSerializer.Deserialize<Shape>(raw2, settings);

            Console.WriteLine(restored1); // Box: Width=10, Height=20
            Console.WriteLine(restored2); // Circle: Radius=30
        }
    }
}


person shadeglare    schedule 03.10.2019    source источник
comment
@shadeglare, см. ответ в соответствующем вопросе (который может быть чище): stackoverflow.com/a/59744873/12509023 Полиморфная (де) сериализация не поддерживается и требует конвертера, подобного тому, что вы написали. Проблема с добавлением поддержки полиморфной сериализации в качестве опции: github.com/dotnet/corefx / issues / 38650   -  person ahsonkhan    schedule 16.01.2020
comment
Вам необходимо избавиться от своего JsonDocument, чтобы избежать утечки памяти, например выполнив using var doc = JsonDocument.Parse(raw); См. примечания к документу.   -  person dbc    schedule 23.07.2020


Ответы (2)


Попробуйте эту библиотеку, которую я написал как расширение для System.Text.Json, чтобы предложить полиморфизм: https://github.com/dahomey-technologies/Dahomey.Json

public abstract class Shape
{
}

[JsonDiscriminator(nameof(Box))]
public class Box : Shape
{
    public float Width { get; set; }

    public float Height { get; set; }

    public override string ToString()
    {
        return $"Box: Width={Width}, Height={Height}";
    }
}

[JsonDiscriminator(nameof(Circle))]
public class Circle : Shape
{
    public float Radius { get; set; }

    public override string ToString()
    {
        return $"Circle: Radius={Radius}";
    }
}

Унаследованные классы должны быть вручную зарегистрированы в реестре соглашения о дискриминаторе, чтобы инфраструктура знала о сопоставлении между значением дискриминатора и типом:

JsonSerializerOptions options = new JsonSerializerOptions();
options.SetupExtensions();
DiscriminatorConventionRegistry registry = options.GetDiscriminatorConventionRegistry();
registry.RegisterConvention(new AttributeBasedDiscriminatorConvention<string>(options, "Tag"));
registry.RegisterType<Box>();
registry.RegisterType<Circle>();

Shape origin1 = new Box { Width = 10, Height = 20 };
Shape origin2 = new Circle { Radius = 30 };

string json1 = JsonSerializer.Serialize(origin1, options);
string json2 = JsonSerializer.Serialize(origin2, options);

Console.WriteLine(json1); // {"Tag":"Box","Width":10,"Height":20}
Console.WriteLine(json2); // {"Tag":"Circle","Radius":30}

var restored1 = JsonSerializer.Deserialize<Shape>(json1, options);
var restored2 = JsonSerializer.Deserialize<Shape>(json2, options);

Console.WriteLine(restored1); // Box: Width=10, Height=20
Console.WriteLine(restored2); // Circle: Radius=30
person Michaël Catanzariti    schedule 06.12.2019

Это сработало для меня (в .Net 5):

JsonSerializer.Serialize<object>(myInheritedObject)

Затем были включены все свойства как базового, так и унаследованного класса. Не уверен, есть ли проблемы с этим подходом ...

person Ilya Chernomordik    schedule 26.11.2020
comment
Отлично работает для самого основного объекта, но имейте в виду, что он не будет иметь такого же эффекта на какие-либо базовые свойства этого объекта. - person Vyrotek; 30.12.2020