Search code examples
c#asp.netjson.netasp.net-core-webapiasp.net-core-3.1

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.


Solution

  • 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.