Skip to content

How to write custom converters for JSON serialization (marshalling) in .NET should explain how to handle complex types #35020

Closed
@konrad-jamrozik

Description

@konrad-jamrozik

The How to write custom converters for JSON serialization (marshalling) in .NET page explains how to use default system converter here.

What is not clear is that these converters are only for primitive types. I was hoping this article will explain how I can do this with my own custom built-in types, but it doesn't.

Following example is given for using a default converter in a custom converter:

public class MyCustomConverter : JsonConverter<int>
{
    private readonly static JsonConverter<int> s_defaultConverter = 
        (JsonConverter<int>)JsonSerializerOptions.Default.GetConverter(typeof(int));

    // Custom serialization logic
    public override void Write(
        Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }

    // Fall back to default deserialization logic
    public override int Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return s_defaultConverter.Read(ref reader, typeToConvert, options);
    }
}

but this doesn't work if value is not int but a custom type. The custom converter will be null and just doing value.ToString() won't produce the right value. Instead, for serialization one has to do:

public override void Write(Utf8JsonWriter writer, MyType value, JsonSerializerOptions options)
{
    writer.WriteRawValue(JsonSerializer.Serialize(value));
}

Similarly, for deserialization:

public override MyType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    return JsonSerializer.Deserialize<MyType>(ref reader);
}

Additional context

You might ask: why I am trying to write a custom type converter for a complex type in the first place? Performance will take a hit.

This is an excellent question. It is because of this limitation:

Quote:

Even though not all reference preservation scenaria contain cycles, it arguably is the raison d'etre of the feature. While we could try to make the feature smarter and only fail if cycles are detected, this would require substantially more state to track correctly and it would impact performance. Consequently, explicitly not supporting the feature at all for constructor deserialization is the right apprioach in my view. As you mention, the workaround is to simply refactor constructor parameters to be init or required properties.

In addition, the documentation on ReferenceHandler.Preserve says:

This feature can't be used to preserve value types or immutable types. On deserialization, the instance of an immutable type is created after the entire payload is read. So it would be impossible to deserialize the same instance if a reference to it appears within the JSON payload.

I am trying to implement my own serialization that will preserve all references and also use constructors, without using required or init properties. This is because I want to forbid using an object initializer because I want to ensure constructor is called, as I am going to put precondition checks in the constructor which would be circumvented otherwise.

Hence, if I could write a custom converter for my complex type I would make it know when to serialize its members fully and when just by reference, to avoid cycles and duplication. It would also have matching deserialization logic which knows when to deserialize given object and when to deserialize just a reference to it and call proper constructors in the right order.

@eiriktsarpalis if you perhaps have some insight how to best deal with my scenario I would be grateful. Maybe there is better approach than what I am thinking.

Related:


Document Details

Do not edit this section. It is required for learn.microsoft.com ➟ GitHub issue linking.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions