1

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.

solublefish
  • 1,681
  • 2
  • 18
  • 24
  • One way to look at this would be: why in the World did you do this? I guess it wasn't your idea? And you cannot change it? – Fildor Aug 30 '23 at 14:06
  • Are you limited to Newtonsoft? Or you can use `System.Text.Json`? – Guru Stron Aug 30 '23 at 14:11
  • Can you process the JSON string before parsing it for real, so that you can change {"A": "B"} into {"A": ["B"]} ? – Andrew Morton Aug 30 '23 at 14:12
  • @GuruStron as mentioned, I could use a different json library, such as System.Text.Json. Do you know of capabilities it has in this area? I'm not as familiar with it, but happy to learn. – solublefish Aug 30 '23 at 15:58
  • @AndrewMorton I suppose I could but I end up with the same problem - I need a mostly-generic way to detect when and where to selectively apply the change. – solublefish Aug 30 '23 at 16:00
  • 1
    @Fildor Right. For my purpose the json format is fixed. – solublefish Aug 30 '23 at 16:02
  • 1
    For Json.NET see [How to handle both a single item and an array for the same property using JSON.net](https://stackoverflow.com/q/18994685/3744182). For [How to handle both a single item and an array for the same property using System.Text.Json?](https://stackoverflow.com/q/59430728/3744182). Your question looks to be a duplicate of one of those two, depending on the serializer you are using. Agree? – dbc Aug 30 '23 at 18:33
  • @dbc yes I agree those look similar; thank you. I'll vote to close mine. I do wish I knew why my google-fu failed me... – solublefish Aug 30 '23 at 21:33

1 Answers1

2

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.

Fildor
  • 14,510
  • 4
  • 35
  • 67
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • This is interesting - I'll try this approach. I somehow wasn't aware you could specify a JsonConverter on a specific property. That could be an acceptable amount of markup for my cases. True, a converter factory is not the prettiest code, but I'd always rather have that stuff in one place than write basically the same logic 42 times. – solublefish Aug 30 '23 at 16:20