Search code examples
serializationjson.net.net-6.0system.text.json

Enum string serialization not working as expected in .net 6.0


I have a production api returning the below json:

[
  {
    "StartUtc": "2023-09-02T13:52:00Z",
    "EndUtc": "2023-09-02T15:19:00Z",
    "isCustomerInStore": false,
    "productLead": {
      "id": 1,
      "leadType": "PREVIOUS_CUSTOMER",
      "leadStatus": "SOLD_DELIVERED",
      "leadStatusType": "SOLD",
      "leadSourceId": 11,
      "leadSourceName": "Previous  Customer"
    },
    "customer": {
      "id": 1,
      "firstName": "Jane",
      "lastName": "Doe",
      "salesRepresentative": {
        "id": 1,
        "firstName": "Sales",
        "lastName": "Guy1",
        "assignedUserType": "SALES_REPRESENTATIVE"
      }
    },
    "product": {
      "year": 2023,
      "make": "Google",
      "model": "Pixel",
      "stockNumber": "ABCD1234"
    }
  }
]

After the serialization of the above json, my api returns the below.

[
  {
    "StartUtc": "2023-09-02T13:52:00Z",
    "EndUtc": "2023-09-02T15:19:00Z",
    "isCustomerInStore": false,
    "productLead": {
      "id": 1,
      "leadType": "previouS_CUSTOMER",
      "leadStatus": "solD_DELIVERED",
      "leadStatusType": "sold",
      "leadSourceId": 11,
      "leadSourceName": "Previous  Customer"
    },
    "customer": {
      "id": 1,
      "firstName": "Jane",
      "lastName": "Doe",
      "salesRepresentative": {
        "id": 1,
        "firstName": "Sales",
        "lastName": "Guy1",
        "assignedUserType": "saleS_REPRESENTATIVE"
      }
    },
    "product": {
      "year": 2023,
      "make": "Google",
      "model": "Pixel",
      "stockNumber": "ABCD1234"
    }
  }
]

The leadType, leadStatus, leadStatusType & assignedUserType are enum fields having attributes [JsonProperty(ItemConverterType = typeof(StringEnumConverter))]. Also, all the enums have the attribute [JsonConverter(typeof(JsonStringEnumConverter))]. But they are returning with first few characters serialized to lower characters? Any ideas why?


Solution

  • "previouS_CUSTOMER" is the camel case serialization of PREVIOUS_CUSTOMER generated by System.Text.Json. Somewhere in your startup method you must be constructing a JsonStringEnumConverter using JsonNamingPolicy.CamelCase, e.g. in Program.cs

    builder.Services.AddControllers()
        .AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
            options.JsonSerializerOptions.Converters.Add(
                new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); // HERE
        });
    

    To prevent this, you have a few options:

    Firstly, if you never want camel case serialization of enums, just don't use JsonNamingPolicy.CamelCase with JsonStringEnumConverter:

    builder.Services.AddControllers()
        .AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
            options.JsonSerializerOptions.Converters.Add(
                new JsonStringEnumConverter());
        });
    

    You can still use camel case for your property names.

    Secondly, if you want camel case serialization of enums for most enums but not your LeadType enum, use OptOutJsonConverterFactory from this answer to Exclude an enum property of a Model from using the JsonStringEnumConverter which is globally set at the Startup? to disable its use for LeadType:

    builder.Services.AddControllers()
        .AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
            options.JsonSerializerOptions.Converters.Add(
                new OptOutJsonConverterFactory(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
                                               typeof(LeadType)));
            // Add a non-camel-case fallback converter for the opted out enum types:
            options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        });
    

    Demo fiddle #1 here.

    Finally, if you don't want camel case enum serialization for the LeadType property, apply [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] to the property like so:

    public class ProductLead
    {
        [System.Text.Json.Serialization.JsonConverter(
            typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
        public LeadType LeadType { get; set; }
        
        // Other properties omitted
    }
    

    Demo fiddle #2 here.

    Notes:

    1. As explained in the docs, converters are chosen with the following precedence:

      • [JsonConverter] applied to a property.
      • A converter added to the Converters collection.
      • [JsonConverter] applied to a custom value type or POCO.

      As you can see, converters added to options supersede converters applied to types (but not properties). This is why applying [JsonConverter(typeof(JsonStringEnumConverter))] to your enum types did not solve the problem.

    2. You wrote that you applied

      [JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
      

      To each property type, but StringEnumConverter is from Newtonsoft.Json, a completely different serializer. System.Text.Json has an attribute JsonConverterAttribute with the same name as Newtonsoft's JsonConverterAttribute so it's easy to get them confused. When using both serializers, I recommend using the fully qualified name for metadata attributes.