10

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
roberto tomás
  • 4,435
  • 5
  • 42
  • 71
  • 2
    You're not using [tag:json.net] you're using [tag:system.text.json]. System.Text.Json does not support `[EnumMember()]` renaming of enum values, you have to write your own custom converter. To do it see [System.Text.Json: How do I specify a custom name for an enum value?](https://stackoverflow.com/a/59061296/3744182). Or, you could switch back to Json.NET as shown in [Where did IMvcBuilder AddJsonOptions go in .Net Core 3.0?](https://stackoverflow.com/a/55666898/3744182). This could be a duplicate of either or both depending upon your preferred serializer. – dbc Jan 31 '20 at 06:31
  • 3
    Did you intend to use Json.NET, or System.Text.Json? [`JsonStringEnumConverter`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter?view=netcore-3.1) is part of System.Text.Json. Newtonsoft uses [`StringEnumConverter`](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Converters_StringEnumConverter.htm). – dbc Jan 31 '20 at 06:34
  • actually, I am thinking the link to switching back to Json.Net is too old.. My Newtsonsoft import is working, yet on that like `services.AddControllers() .AddNewtonsoftJson();` I get an error that AddNewtonsoftJson is not a member of IMVCBuilder. This is the sort of problem that deserves being written about in rsponse to a real answer, could you provide one? – roberto tomás Jan 31 '20 at 12:35
  • @dbc re your second suggestion. thank you. I tried using Newtonsoft.Json.Converters and StringEnumConverter .. sadly, this continues to fail – roberto tomás Jan 31 '20 at 12:38
  • I simplified your question by editing out the intermediate update that followed up on my suggestion to use `CustomJsonStringEnumConverter`. That converter isn't going to work here; I updated my answer to [System.Text.Json: How do I specify a custom name for an enum value?](https://stackoverflow.com/a/59061296/3744182) to indicate that my `CustomJsonStringEnumConverter` only works for serialization, and to provide some alternative suggestions for deserialization. Feel free to revert my edit if it wasn't appropriate. – dbc Feb 01 '20 at 23:52

3 Answers3

8

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 does not yet 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.

dbc
  • 104,963
  • 20
  • 228
  • 340
2

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");
Mikael Dúi Bolinder
  • 2,080
  • 2
  • 19
  • 44
0

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 ;)

lazyList
  • 541
  • 5
  • 5