0

Using System.Text.Json, I am writing a custom JsonConverter<T>.Read() deserialization method to deserialize a JSON object. The method reads each property name and value from the JSON and manually assigns the results into the deserialized object, along the lines of the example shown in the Microsoft documentation How to write custom converters for JSON serialization (marshalling) in .NET: Support polymorphic deserialization. However, in my case the JSON object will sometimes contain unknown properties that I want to simply ignore. When this happens, the example code in the documentation will cause an exception to be thrown:

JsonException: The converter 'PersonConverter' read too much or not enough.

How can I correctly skip past unknown properties in a custom object deserializer?

A minimal example follows. Say I have the following data model:

public class Person
{
    public string Name { get; set; }
}

And the following converter:

public class PersonConverter : JsonConverter<Person>
{
    public override bool CanConvert(Type typeToConvert) =>
        typeof(Person).IsAssignableFrom(typeToConvert);

    public override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return null;
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();

        var person = new Person();

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return person;

            if (reader.TokenType == JsonTokenType.PropertyName)
            {
                var propertyName = reader.GetString();
                reader.Read();
                switch (propertyName)
                {
                    case "Name":
                        person.Name = reader.GetString();
                        break;
                }
            }
        }

        throw new JsonException();
    }

    public override void Write(Utf8JsonWriter writer, Person person, JsonSerializerOptions options) => throw new NotImplementedException();
}

When attempting to deserialize the following JSON:

{"Name":"my name", "ExtraData" : {"Value" : "extra value"} }

Via:

var json = @"{""Name"":""my name"", ""ExtraData"" : {""Value"" : ""extra value""} }";
var options = new JsonSerializerOptions
{
    Converters = { new PersonConverter() },
};
var person = JsonSerializer.Deserialize<Person>(json, options);

The following exception is thrown:

System.Text.Json.JsonException: The converter 'PersonConverter' read too much or not enough. Path: $ | LineNumber: 0 | BytePositionInLine: 58.
   at System.Text.Json.ThrowHelper.ThrowJsonException_SerializationConverterRead(JsonConverter converter)
   at System.Text.Json.Serialization.JsonConverter`1.VerifyRead(JsonTokenType tokenType, Int32 depth, Int64 bytesConsumed, Boolean isValueConverter, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](Utf8JsonReader& reader, Type returnType, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, Type returnType, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)

How can I correct the logic of the converter to avoid this exception?

A simplified demo fiddle is here: fiddle #1.

A demo of the converter from the documentation throwing the same exception is here: fiddle #2.

dbc
  • 104,963
  • 20
  • 228
  • 340

1 Answers1

3

When an unknown property is encountered inside JsonConverter<T>.Read(), one must call Utf8JsonReader.Skip() to advance the reader as required to ignore the unknown value and its children. This method

Skips the children of the current JSON token.

The call to Skip() should be added into the property name switch statement as a default case like so:

public override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    if (reader.TokenType == JsonTokenType.Null)
        return null;
    else if (reader.TokenType != JsonTokenType.StartObject)
        throw new JsonException();

    var person = new Person();

    while (reader.Read())
    {
        if (reader.TokenType == JsonTokenType.EndObject)
            return person;
        else if (reader.TokenType != JsonTokenType.PropertyName)
            throw new JsonException();

        var propertyName = reader.GetString();
        reader.Read();            // Advance the reader to the property value.
        switch (propertyName)
        {
            case "Name":
                person.Name = reader.GetString();
                break;
            default:
                reader.Skip();    // Advance the reader as required to skip the unknown value
                break;
        }
    }
    throw new JsonException();    // Malformed truncated file
}  

Notes:

  • Rather than checking to see whether the current token is a property name inside the Read() loop, it is better to throw an exception if it is not. A well-formed JSON object is defined to consist of name/value pairs, and if the token is not a property name that likely indicates a bug in the reader that could lead to data loss.

  • In practice, the code in the documentation skips unknown properties successfully when their values are primitives (numbers, strings, Booleans or null) and fails when their values are objects or arrays.

A demo of the fixed simplified converter can be found here: fix #1.

A fixed version of the documentation converter can be found here: fix #2.

dbc
  • 104,963
  • 20
  • 228
  • 340