System.Text.Json won't deserialize to my type even though my constructor implements every property
Asked Answered
T

2

7

I can't figure out why System.Text.Json won't deserialize this simple JSON string to my specified .NET type. I'm getting the Each parameter in the deserialization constructor on type 'MenuItem' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. The match can be case-insensitive error message, even though I have mapped every property — name, url, and permissions — in my MenuItem constructor.

Dotnet fiddle demo

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

public class Program
{
    public static void Main()
    {
        try
        {
            var json = @"
                [
                    {
                        ""name"": ""General Info"",
                        ""url"": ""/Info"",
                        ""permissions"": []
                    },
                    {
                        ""name"": ""Settings"",
                        ""url"": ""/Settings"",
                        ""permissions"": [
                            ""Admin""
                        ]
                    }
                ]";

            Console.WriteLine(json);
            var menu = new Menu(json);
            Console.WriteLine("Deserialization success");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

public class Menu
{
    public Menu(string json)
    {
        var items = JsonSerializer.Deserialize<List<MenuItem>>(json);
        MenuItems = items.AsReadOnly();
    }

    public IReadOnlyList<MenuItem> MenuItems { get; }
}

public class MenuItem
{
    [JsonConstructor]
    public MenuItem
        (
            string name, 
            string url, 
            List<Permission> permissions
        )
    {
        this.Name = name;
        this.Url = url;
        this.Permissions = (permissions ?? new List<Permission>()).AsReadOnly();
    }

    public string Name { get; }
    public string Url { get; }
    public IReadOnlyList<Permission> Permissions { get; }
}

public enum Permission
{
    Admin,
    FinancialReporting
}

Can anyone tell me what I'm doing wrong?

Tortious answered 18/2, 2022 at 23:37 Comment(2)
Your constructor and property names differ in case. Try setting JsonSerializerOptions.PropertyNameCaseInsensitive = true as shown in JsonSerializer.Deserialize fails.Inga
Very strange. Not only does the error message already say the match can be case insensitive, but because I’m deserializing through a custom constructor, it should only care about the parameter names, not the property names. I’ll test this out later once I’m near my computerTortious
T
8

I've had a look at your fiddle and spotted a couple of problems. Working fiddle here

  1. System.Text.Json is case-sensitive by default (except for web apps). You can resolve this by using either PropertyNamingPolicy = JsonNamingPolicy.CamelCase or PropertyNameCaseInsensitive = true in the serializer options.

  2. The second issue is outlined in Enums as strings

    By default, enums are serialized as numbers. To serialize enum names as strings, use the JsonStringEnumConverter.

    You should add JsonSerializerOptions to resolve (1) and (2):

    var options = new JsonSerializerOptions 
    {
        Converters =
        {
            new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
        },
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
    var items = JsonSerializer.Deserialize<List<MenuItem>>(json, options);
    
  3. The third issue appears to be with the binding in the constructor for the list of Permissions. In the constructor you define a List<Permission> for the permissions parameter. I receive the error in your question unless the constructor argument type matches the model property type exactly. So, I updated the constructor to take a IReadOnlyList<Permission> and it deserializes successfully:

    [JsonConstructor]
    public MenuItem
        (
            string name, 
            string url,
            IReadOnlyList<Permission> permissions
        )
    {
        this.Name = name;
        this.Url = url;
        this.Permissions = permissions ?? new List<Permission>().AsReadOnly();
    }
    

    Alternatively, you could change the Permissions property to List<Permission>.

This answer to a question with a similar problem explains that this is actually a limitation of System.Text.Json and there is currently an open github issue.

A working fork of your fiddle is demoed here.

Trotter answered 19/2, 2022 at 12:54 Comment(6)
Am I right in assuming that if I want enums to be able to dynamically serialize to either string or numeric data types depending on the target type, I'd have to write a custom converter that uses reflection? And if so, would that reflection efficiently run just once, or does it happen O(n) times?Tortious
@JacobStamm did you try it? It works in my testing. To test, just edit the fiddle in my answer - you can add int and string values to the permissions array and it deserializes both correctly. Are you seeing different?Trotter
@JacobStamm maybe I misinterpreted your question. You can serialize enum values using the JsonStringEnumConverter in the options (just like deserializing) - if you want ints then serialize without the converter, if you want strings then use the converter in the serialization: JsonSerializer.Serialize(MenuItems, options);Trotter
You're right, it works with both string and int values once the string converter is added. Thank you.Tortious
Back to the non-overengineered Newtonsoft it is...Granger
PropertyNameCaseInsensitive = true solved the problem for me. Thanks a ton!Beverlee
H
0

I had the same issue. The solution is to use

JsonSerializable

attribute of

System.Text.Json.Serialization

It looks like:

[JsonSerializable(typeof(YourClassName))]
public class YourClassName
{
    public byte YourPropertyName { get; set; }
}

Always class and always public and always properties and always use the attribute on the class.

Heyday answered 15/2, 2023 at 15:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.