roberto tomás
roberto tomás

Reputation: 4687

deserialize json with array of enum

Using the enum:

namespace AppGlobals
{
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public enum BoardSymbols
    {
        [EnumMember(Value = "X")]
        First = 'X',
        [EnumMember(Value = "O")]
        Second = 'O',
        [EnumMember(Value = "?")]
        EMPTY = '?'
    }
}

I would like to define a model for my api:

using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Newtonsoft.Json;

namespace Assignment_1
{
    public class MyRequest
    {
//...
        [Required]
        [MinLength(9)]
        [MaxLength(9)]
        [JsonProperty("changeTypes", ItemConverterType = typeof(JsonStringEnumConverter))]
        public AppGlobals.BoardSymbols[] GameBoard { get; set; }
    }
}

Where GameBoard should serialize to JSON as an array of strings with names specified by the EnumMember attributes. This approach is adapted from Deserialize json character as enumeration. However, it does not work. This does works if I change the enum to:

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public enum BoardSymbols
    {
      X='X',
      Y='Y'
    }

But I obviously hit a limit on the 'empty' enumeration. How can I do this?

update 2:

I did not have AddNewtonsoftJson() in my startup, converting over fully to Newtonsoft. Now my error is perhaps more actionable:

System.InvalidCastException: Unable to cast object of type 'CustomJsonStringEnumConverter' to type 'Newtonsoft.Json.JsonConverter'.
   at Newtonsoft.Json.Serialization.JsonTypeReflector.CreateJsonConverterInstance(Type converterType, Object[] args)

This makes sense, the solution prescribed to me here specified a JsonConverterFactory .. I just need the raw JsonConverter for my use case instead.

Upvotes: 12

Views: 13265

Answers (3)

dbc
dbc

Reputation: 116794

TL/DR: You have two basic problems here:

  1. .NET Core 3.0+ has a new built-in JSON serializer System.Text.Json, and you are mixing up attributes and classes between this new serializer and Json.NET. This is very easy to do when both are installed because they share some class names, such as JsonSerializer and JsonConverter.

  2. The new serializer is used by default but, prior to .NET 9, does not support serialization of enums as strings with custom value names; see System.Text.Json: How do I specify a custom name for an enum value? for details.

The easiest way to solve your problem is to switch back to Json.NET as shown here and use attributes, converters and namespaces exclusively from this serializer.

First let's break down the differences and similarities between the two serializers:

  1. System.Text.Json:

  2. Json.NET:

With this in mind, which serializer are you using in your code? Since you helpfully included the namespaces in your question, we can check:

using System.Text.Json.Serialization; // System.Text.Json
using Newtonsoft.Json;                // Json.NET

namespace Assignment_1
{
    public class MyRequest
    {
//...
        [JsonProperty(                                         // JsonProperty from Newtonsoft
            "changeTypes", 
            ItemConverterType = typeof(JsonStringEnumConverter)// JsonStringEnumConverter from System.Text.Json
        )]
        public AppGlobals.BoardSymbols[] GameBoard { get; set; }
    }
}

So as you can see, you are mixing up attributes from Newtonsoft with converters from System.Text.Json, which isn't going to work. (Perhaps you selected the namespaces from a "Resolve -> using ..." right-click in Visual Studio?)

So, how to resolve the problem? Since Json.NET supports renaming of enum values out of the box, the easiest way to resolve your problem is to use this serializer. While possibly not as performant as System.Text.Json it is much more complete and full-featured.

To do this, remove the namespaces System.Text.Json.Serialization and System.Text.Json and references to the type JsonStringEnumConverter from your code, and modify MyRequest and BoardSymbols as follows:

using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json;

namespace Assignment_1
{
    public class MyRequest
    {
//...
        [Required]
        [MinLength(9)]
        [MaxLength(9)]
        [JsonProperty("changeTypes")] // No need to add StringEnumConverter here since it's already applied to the enum itself
        public AppGlobals.BoardSymbols[] GameBoard { get; set; }
    }
}

namespace AppGlobals
{
    [JsonConverter(typeof(StringEnumConverter))]
    public enum BoardSymbols
    {
        [EnumMember(Value = "X")]
        First = 'X',
        [EnumMember(Value = "O")]
        Second = 'O',
        [EnumMember(Value = "?")]
        EMPTY = '?'
    }
}

Then NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson and in Startup.ConfigureServices call AddNewtonsoftJson():

services.AddMvc()
    .AddNewtonsoftJson();

Or if you prefer to use StringEnumConverter globally:

services.AddMvc()
    .AddNewtonsoftJson(o => o.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()));

Do take note of the following comment from the docs

Note: If the AddNewtonsoftJson method isn't available, make sure that you installed the Microsoft.AspNetCore.Mvc.NewtonsoftJson package. A common error is to install the Newtonsoft.Json package instead of the Microsoft.AspNetCore.Mvc.NewtonsoftJson package.

Mockup fiddle here.

Upvotes: 9

lazyList
lazyList

Reputation: 571

Here is a custom converter to deserialize a list of strings ( ex. from your POST payload) to a list of enums , using JsonConverterFactory.

public class ListOfEnumConverter : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {

        if (!typeToConvert.IsGenericType)
        {
            return false;
        }

        if (typeToConvert.GetGenericTypeDefinition() != typeof(List<>))
        {
            return false;
        }

        return typeToConvert.GetGenericArguments()[0].IsEnum;
    }

    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        Type enumType = typeToConvert.GetGenericArguments()[0];

        JsonConverter converter = (JsonConverter)Activator.CreateInstance(
            typeof(ListOfEnumConverterInner<>).MakeGenericType(
                new Type[] { enumType }),
            BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            args: new object[] { options },
            culture: null)!;

        return converter;
    }
}
public class ListOfEnumConverterInner<TEnum> :
    JsonConverter<List<TEnum>> where TEnum : struct, Enum
{
    private readonly JsonConverter<TEnum> _itemConverter;
    private readonly Type _itemType;
    public ListOfEnumConverterInner(JsonSerializerOptions options)
    {
        // For performance, use the existing converter.
        _itemConverter = (JsonConverter<TEnum>)options
                .GetConverter(typeof(TEnum));

        // Cache the enum types.
        _itemType = typeof(TEnum);
    }

    public override List<TEnum>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 
    {
        if (reader.TokenType != JsonTokenType.StartArray)
        {
            throw new JsonException();
        }

        var enumList = new List<TEnum>();

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndArray)
            {
                return enumList;
            }
            // Get the item.
            if (reader.TokenType != JsonTokenType.String)
            {
                throw new JsonException();
            }

            string? nextItem = reader.GetString();

            // For performance, parse with ignoreCase:false first.
            if (!Enum.TryParse(nextItem, ignoreCase: false, out TEnum item) &&
                !Enum.TryParse(nextItem, ignoreCase: true, out item))
            {
                throw new JsonException(
                    $"Unable to convert \"{nextItem}\" to Enum \"{_itemType}\".");
            }

            //add to list now
            enumList.Add(item);
        }

        throw new JsonException();
    }

    public override void Write(Utf8JsonWriter writer, List<TEnum> enumList, JsonSerializerOptions options)
    {
        writer.WriteStartArray();

        foreach (TEnum item in enumList)
        {
            var nextItem = item.ToString();
            writer.WriteStringValue
                (options.PropertyNamingPolicy?.ConvertName(nextItem) ?? nextItem);

            _itemConverter.Write(writer, item, options);
        }

        writer.WriteEndArray();
    }
}

Note: This can also serialize a List of enums, to a list of strings.

Now, all you need to do is to decorate your input model properties with attributes, that point to this converter like this

public class ModelWithEnum
{
    public int test1 { get; set; }

    [json.JsonConverter(typeof(JsonStringEnumConverter))]
    public ServiceType test2 { get; set; }

    [json.JsonConverter(typeof(ListOfEnumConverter))]
    public List<ServiceType> test3 { get; set; }
}

Hope this helps !

Give me a thumbs-up, if this saves you few hours ;)

Upvotes: 3

Mikael D&#250;i Bolinder
Mikael D&#250;i Bolinder

Reputation: 2284

You could create you own JsonStringEnumAttribute and decorate your enum with it.

using System.Text.Json.Serialization;

class JsonStringEnumAttribute : JsonConverterAttribute
{
    public JsonStringEnumAttribute() : base(typeof(JsonStringEnumConverter))
    {

    }
}

Then put it on your enum:

[JsonStringEnum]
enum MyEnum
{
    Value1,
    Value2
}

You can then deserialize JSON like this with the string values:

{
    "MyEnumProperty1": "Value1",
    "MyEnumProperty2": ["Value2", "Value1"]
}

Into a class like this:

class MyClass
{
    MyEnum MyEnumProperty1 { get; set; }
    MyEnum[] MyEnumProperty2 { get; set; }
}

Using, for example, System.Net.Http.Json:

using HttpClient client = new();
var myObjects = await client.GetFromJsonAsync<MyClass>("/some-endpoint");

Upvotes: 2

Related Questions