Search code examples
c#jsonasp.net-corejson.netasp.net-core-webapi

How can I deserialize json that may be a scalar and may be an array?


I have json string, representing a json object with a value that may be a string or may be an array of strings. That is, the json may be like this:

{ "MyProperty" : "only-value" }

or may be like this:

{ "MyProperty" : ["first-value", "another-value", "yep-one-more-value"] }

I want to deserialize this json into an object like this:

public record MyType(IReadOnlyCollection<string> MyProperty);   

I'm on C# 10, but if you speak an older version, this form may be more familiar, and would work for me if necessary:

public class MyType2
{
    public List<string>? MyProperty { get; set; }
} 

For that matter, feel free to answer in another .NET language - I can probably translate and it might be useful to other readers.

When I'm done, I want MyProperty to be a collection containing N strings. In the first example it should contain one string ("only-value"), and 3 strings in the second example.

Of course my first attempt was to hope Json.Net was clever enough to magically do what I want:

var myObj = JsonConvert.DeserializeObject<MyType>(jsonStr);

But naturally it can't handle the scalar case, returning the error: Error converting value "only-value" to type 'System.Collections.Generic.IReadOnlyCollection`1[System.String]'. Path 'MyProperty', line 1, position 28.

I'm aware I can solve this specific case with a custom JsonConverter that applies to MyType. It can scan the json at the token level, detect the scalar versus vector value, and respond appropriately. I've verified this works for MyType above.

But I have many objects like this, some with multiple such either-or properties. I really would like a generic solution, or at least one with less maintenance overhead and boilerplate code.

I'm using Json.Net but would consider using another json library if there's something out there that will do the job without too much fuss.


Solution

  • I'm aware I can solve this specific case with a custom JsonConverter that applies to MyType

    You can also do this by creating a custom converter that applies to collection. For example for System.Text.Json:

    public class ListOrSingleValueConverter<TElement> : JsonConverter<IReadOnlyCollection<TElement>>
    {
        public override IReadOnlyCollection<TElement> Read(ref Utf8JsonReader reader, Type typeToConvert,
            JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.StartArray)
            {
                var list = JsonSerializer.Deserialize<List<TElement>>(ref reader, options);
                if (list is null)
                {
                    return default;
                }
    
                return list;
            }
    
            return new List<TElement> { JsonSerializer.Deserialize<TElement>(ref reader, options) };
        }
    
        public override void Write(Utf8JsonWriter writer, IReadOnlyCollection<TElement> value, JsonSerializerOptions options) => throw new NotImplementedException();
    }
    

    And usage:

    public record MyType([property: JsonConverter(typeof(ListOrSingleValueConverter<string>))]IReadOnlyCollection<string> MyProperty);
    
    var jsStr = """
    [{ "MyProperty" : "only-value" },{ "MyProperty" : ["first-value", "another-value", "yep-one-more-value"] }]
    """;
    var myTypes = JsonSerializer.Deserialize<List<MyType>>(jsStr);
    

    Which you can try to generalize even more via converter factory which also can allow managing collection types but can become a bit more cumbersome.