Eric Rini
Eric Rini

Reputation: 1890

JsonConstructor fails on IEnumerable property?

So setting an enumerable using an accessor works great.

public class SetEnumerableWithAccessor
{
    public IEnumerable<string>? Strings { get; set; }
}

string json = JsonSerializer.Serialize(new SetEnumerableWithAccessor
{
    Strings = new [] { "test" }
});

JsonSerializer.Deserialize<SetEnumerableWithAccessor>(json);

Setting an enumerable using a constructor fails.

public class SetEnumerableWithConstructor
{
    [JsonConstructor]
    public SetEnumerableWithConstructor(IEnumerable<string> strings)
    {
        Strings = strings.ToImmutableList();
    }
    
    public ImmutableList<string> Strings { get; }
}

string json = JsonSerializer.Serialize(new SetEnumerableWithConstructor(new string[] { "test" }));
JsonSerializer.Deserialize<SetEnumerableWithConstructor>(json);

This is the exception that is thrown.

System.InvalidOperationException
Each parameter in the deserialization constructor on type 'CertusLogic.Audio.Test.FourierTransformTest+SetEnumerableWithConstructor' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. The match can be case-insensitive.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(Type parentType)
   at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   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.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)

Really banging my head on something that feels like it should be pretty straightforward.

Upvotes: 1

Views: 1183

Answers (1)

dbc
dbc

Reputation: 116795

When deserializing a class with a parameterized constructor, Sytem.Text.Json requires that constructor parameters match with serialized properties. From the docs [1]:

The parameter names of a parameterized constructor must match the property names and types. Matching is case-insensitive, and the constructor parameter must match the actual property name even if you use [JsonPropertyName] to rename a property.

Thus you must add a constructor to your class with an ImmutableList<string> strings argument and mark that constructor, and only that constructor, with [JsonConstructor]:

public class SetEnumerableWithConstructor
{
    [JsonConstructor]
    public SetEnumerableWithConstructor(ImmutableList<string> strings) => Strings = strings;

    public SetEnumerableWithConstructor(IEnumerable<string> strings) => Strings = strings.ToImmutableList();

    public ImmutableList<string> Strings { get; }
}

And now your class will be serializable. Demo fiddle here: https://dotnetfiddle.net/wo5TlF.

Note that there is currently an enhancement [JsonSerializer] Relax restrictions on ctor param type to immutable property type matching where reasonable #44428 open asking to loosen the argument type matching restrictions, with nothing scheduled as of April 2023.


[1] The documentation was updated in 2023. At the time this question was asked, the documentation merely stated

The parameter names of a parameterized constructor must match the property names.

Upvotes: 5

Related Questions