DEV Community

Cover image for Array or object JSON deserialization (feat. .NET & System.Text.Json)
João Antunes
João Antunes

Posted on • Originally published at blog.codingmilitia.com on

Array or object JSON deserialization (feat. .NET & System.Text.Json)

Intro

Ah, the joys of integrating with third-party APIs... We always end up having to hammer something to get things working 🤣.

This is a post about one of such situations, resorting to some JSON deserialization trickery (via JsonConverter) to be able to get things working.

Problem statement

So, I’m integrating with this API, which in a specific endpoint, for some reason, returns an object in which one of its properties, when empty, is an empty array, but when there’s data, it’s an object with an items property which in turn is the array.

Some examples, for better understanding:

When collection is empty:

{
    "someItems": []
}
Enter fullscreen mode Exit fullscreen mode

When collection has entries:

{
    "someItems": {
        "items": [
            {
                "id": 123,
                "text": "some text"
            },
            {
                "id": 456,
                "text": "some more text"
            }
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

Let’s just say that the deserializer wasn’t happy when this happened 🙂.

So, how do we get out of this mess? Enter JsonConverters.

Side note: I reported this behavior as a bug, which will be fixed at some point, but until then, I still need to get things working.

Implementing a JsonConverter

Let’s jump right into the code!

public class ArrayOrObjectJsonConverter<T> : JsonConverter<IReadOnlyCollection<T>>
{
    public override IReadOnlyCollection<T>? Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
        => reader.TokenType switch
        {
            JsonTokenType.StartArray => JsonSerializer.Deserialize<T[]>(ref reader, options),
            JsonTokenType.StartObject => JsonSerializer.Deserialize<Wrapper>(ref reader, options)?.Items,
            _ => throw new JsonException()
        };

    public override void Write(Utf8JsonWriter writer, IReadOnlyCollection<T> value, JsonSerializerOptions options)
        => JsonSerializer.Serialize(writer, (object?) value, options);

    private record Wrapper(T[] Items);
}
Enter fullscreen mode Exit fullscreen mode

Fortunately, as we can see, it’s not that much code, so we’ll go through it quickly.

We’re inheriting from JsonConverter<IReadOnlyCollection<T>>, to implement the JSON converter for that specific type (I normally use IReadOnlyCollection when passing collections around instead of IEnumerable, to be sure the collection isn’t lazy unless I really want it to be).

As for the read implementation, we check what the first token of the object’s JSON representation is:

  • Array start token ([) - we deserialize it as an array
  • Object start token ({) - we deserialize it as the Wrapper type declared below, which fits the structure of the objects we’re receiving
  • Another thing - that’s unexpected, so blowing things up 💥

Note that the usage of T[] is important. If we used IReadOnlyCollection<T>, we’d get into an infinite loop, with the converter basically calling itself again and again. I used T[] here, but it could also be another type of collection, just can’t be IReadOnlyCollection, to avoid the loop.

Regarding writing, it wasn’t important in my case, as at least for now I only need to deserialize things, but anyway, created a simple implementation where it’s always serialized as a JSON array. We could easily adjust it a bit if we wanted to serialize in different ways, depending on some logic.

Again, note that the cast to object? is important, for the same reason we used T[] earlier, we end up in a loop because the value is of type IReadOnlyCollection<T>. Casting it like this, the serializer will care for the underlying type, being oblivious to the original type of the object reference. We could achieve the same in some other ways, like casting to IEnumerable<T>, or removing the cast altogether and pass the generic parameter like JsonSerializer.Serialize<object?>(writer, value, options).

Outro

That’s it for this quick post.

We saw how to make use of JsonConverters to customize (de)serialization of specific types, in this case to workaround a bug in an API, but it’s also useful for a lot of other situations.

I’ve been dabbling with JsonConverters a bunch lately, so I’ll probably share one or two more of these posts, to document my shenanigans.

Fortunately, it ended up being less complicated than I expected, so I was happy not to waste too much time hammering things to hide bugs.

Links in the post:

Thanks for stopping by, cyaz! 👋

Oldest comments (0)