2

I'm currently in the process of migrating vom Newtonsoft to System.Text.Json. Newtonsoft was able to automatically deserialize objects, that have one or more Interface properties. With System.Text.Json I get the following error message for the respective classes when I try to accomplish the same:

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.

I am able to avoid this problem by writing custom converters, but this would result in a lot of overhead for objects with multiple nested layers, where each Interface property can again have multiple Interface properties on their own (thus requiring multiple custom converters). Is there a simpler solution for that problem?

I create an example to illustrate the problem:

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

With these custom converters I get the intended behaviour. But is there a way to avoid writing a custom converter for each such classes?

Steve
  • 23
  • 2
  • Your Constructors take concrete types but then surface them as just the Interface. So you should try using the interface also in the constructor (Handing over IChild not Child and IName not Name) so the signature of the constructor fits the surface of the class. – Ralf Jun 22 '23 at 07:57
  • Deserialization of interface types is not supported in System.Text.Json, so that would not help my problem. – Steve Jun 22 '23 at 08:55
  • Ah. I see. It would have fixed the highlighted error message you showed. Maybe you want to remove it as it distracts from the problem you actually have. – Ralf Jun 22 '23 at 09:15
  • As you are asking for something easier. Why not just go on using Json.Net. As far as i understand it even Microsoft sees System.Text.Json as another option not as a replacement. – Ralf Jun 22 '23 at 09:16
  • The main reasons are consistency and not running into any unwanted behaviour by using multiple different libraries. The motivation for switching to System.Text.Json is mostly performance improvement. Thanks for your input though! – Steve Jun 22 '23 at 09:21

1 Answers1

3

This is called polymorphic serialization. The library needs some way to annotate what specific implementation of IChild a property actually is. I think Newtonsoft adds a property with the complete type name, while this is easy to use, it has some potential downsides if you want to rename your class.

System.Text.Json uses attributes to annotate the derived types, and require you to supply the identifiers. Read more about Polymorphic type discriminators.

    [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; }
    }

If you only have one implementation of each interface I would consider either getting rid of the interfaces, or converting your objects to Data Transfer Objects (DTOs) that are as simple to serialize as possible. Using DTOs helps separate the concerns of serializing from from any logic in your classes.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • First of all, thanks for your quick reply. Unfortunately, it looks like this feature is not available for .NET 6. I will look into restructuring the code (e.g. removing unnecessary interfaces), but if that is not possible I guess i will have to build a workaround, similar to the one I posted. – Steve Jun 22 '23 at 09:01
  • @Steve This should be available in at least System.Text.Json 7.0.2, since I have tested it there. And that version should be compatible all the way back to .net 4.6.2. The documentation does not make this super clear however. – JonasH Jun 22 '23 at 09:34
  • Ah okay, I unfortunately still get the same exception when annotating the IChild interface with: [JsonDerivedType(typeof(Child), typeDiscriminator: "Child")] and the IName interface with: [JsonDerivedType(typeof(Name), typeDiscriminator: "Name")] – Steve Jun 22 '23 at 10:04
  • Okay, when changing the constructor parameter types from Child to IChild and Name to IName it seems to work. Thank you very much for your help! – Steve Jun 22 '23 at 10:14
  • When writing this example I forgot to consider one thing: In my real application I get the json from a http request and don't serialize it on my own. The json is then not enriched with the necessary meta data to correctly deserialize. Any solution for that? – Steve Jun 22 '23 at 12:04
  • @Steve I'm not sure. If you do not have type information you cannot support polymorphism. I would just use the dto-pattern to handle cases like that. – JonasH Jun 22 '23 at 12:13