How to convert json array to collection with System.Text.Json;

Viewed 642

I have a User class that has a property Roles of type ISet<Role>, where Role is an enum type. In JSON I get an array for that property ("Roles":["Admin","User"], for example). How can I write a custom converter to convert this array into ISet collection?

I've tried to write this class:

class CustomStringToRoleConverter : JsonConverter<ISet<User.Role>> {
    public override ISet<User.Role> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
        return null;
    }

    public override void Write(Utf8JsonWriter writer, ISet<User.Role> roles, JsonSerializerOptions options) {
        StringBuilder builder = new StringBuilder();
        builder.Append(string.Join(",", roles.Select(role => role.ToString()).ToArray()));
        writer.WriteStartArray();
        writer.WriteStringValue(builder.ToString());
        writer.WriteEndArray();
    }
}

but I don't know what to do in Read method. How do I get the array from Utf8JsonReader? There is a GetString method, but it throws exception when I call it:

InvalidOperationException: Cannot get the value of a token type 'StartArray' as a string.

I am using System.Text and System.Text.Json.

1 Answers

The JSON "Roles":["Admin","User"] property is an array of string values, not a single string value containing comma-separated values. Thus the easiest way to write your custom converter is as follows:

class CustomStringToRoleConverter : JsonConverter<ISet<User.Role>> {
    public override ISet<User.Role> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        JsonSerializer.Deserialize<List<string>>(ref reader, options)
            ?.Select(s => Enum.Parse<User.Role>(s))
            ?.ToHashSet(); // Or use a SortedSet if you prefer

    public override void Write(Utf8JsonWriter writer, ISet<User.Role> roles, JsonSerializerOptions options) =>
        JsonSerializer.Serialize(writer, roles.Select(r => r.ToString()), options);
}

The converter simply serializes and deserializes a collection of strings, then parses or formats the items to the Role enum as a post- or pre-processing step. (For comparison, your existing Write() method would generate "Roles": ["Admin,User"] which does not match the JSON shown in the starting paragraph of your question.)

Sample fiddle #1 here.

If you are very concerned with performance and would prefer a more "manual" approach that does not deserialize to an intermediate List<string>, you could do:

class CustomStringToRoleConverter : JsonConverter<ISet<User.Role>> {
    public override ISet<User.Role> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return null; // Or throw an exception if you don't want to allow null
        else if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException();
        var set = new HashSet<User.Role>();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndArray)
                return set;
            else if (reader.TokenType == JsonTokenType.String)
                set.Add(Enum.Parse<User.Role>(reader.GetString()));
            else
            {
                //reader.Skip();
                throw new JsonException(); // Unexpected token;
            }
        }
        throw new JsonException(); // Truncated file;
    }

    public override void Write(Utf8JsonWriter writer, ISet<User.Role> roles, JsonSerializerOptions options)
    {
        writer.WriteStartArray();
        foreach (var value in roles)
            writer.WriteStringValue(value.ToString());
        writer.WriteEndArray();
    }
}

Sample fiddle #2 here.

Alternatively, you could simply add JsonStringEnumConverter to JsonSerializerOptions.Converters to serialize all enums as strings.

Related