1

I am trying to use the JsonPropertyName in combination with the JsonConstuctor attributes from System.Text.Json to add json serialization to an existing library.

Each type has a constructor which mostly matches the readonly fields (and/or get only properties). The issue is that the names don't always match and I don't want to rename properties/arguments on public members.

So a class/struct may look like:

public struct Person
{
    public Person(string Name)
    {
        FirstName = Name
    }

    public string FirstName { get; }
}

I would have thought this would have worked:

public struct Person
{
    [JsonConstructor]
    public Person(string Name)
    {
        FirstName = Name
    }

    [JsonPropertyName("Name")]
    public string FirstName { get; }
}

the json output looks correct

{
   "Name": "Joe"
}

But on deserialization the following error is thrown.

'Each parameter in the deserialization constructor on type 'Person' 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.'

As i write this i am now thinking that the [JsonConstructor] attribute is not forcing the deserializer to call the constructor and matching json keys with argument names but rather using the names from the constructor to set only those properties which match in name. In any case is there are way without modifying the Person struct (ideally using attributes) to support System.Text.Json serialization?

4 Answers 4

4

If you rename that constructor parameter from Name to firstName then it all works fine using System.Text.Json.
(FirstName would also work, but camel case is more common for parameters.)

Doing so, the constructor argument firstName matches the name of the FirstName property, just as mentioned in that exception message.

Each parameter name must match with a property or field on the object. The match can be case-insensitive.

public struct Person
{
    [JsonConstructor]
    public Person(string firstName)
    {
        FirstName = firstName;
    }

    [JsonPropertyName("Name")]
    public string FirstName { get; }
}

var json = @"{ ""Name"": ""Joe"" }";
var person = JsonSerializer.Deserialize<Person>(json);

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

1

I have never seen System.Text.Json.JsonConstructor working properly, when I see even one example that is working I will let you know. So I recommend you to send an email to Microsoft to thank them for an amazing parser and use Newtonsoft.Json

using Newtonsoft.Json;

Person person =Newtonsoft.Json.JsonConvert.DeserializeObject<Person>(json);

public struct Person
{
    [Newtonsoft.Json.JsonConstructor]
    public Person(string Name)
    {
        FirstName = Name;
    }

    [JsonProperty("Name")]
    public string FirstName { get; }
}

1 Comment

Well this is incredibly frustrating to discover. This is an exact copy using Newtonsoft and works as i expected it to. What's frustrating is that i wanted to add these attributes to the library in order to allow for json serialization with no restriction on Newtonsoft vs System.Text.Json.
1

If you are able to change the existing library, then using C# 9 you can add a private init; and do the following:

public struct Person
{
    public Person(string Name)
    {
        this.FirstName = Name;
    }
    
    [JsonInclude]
    [JsonPropertyName("Name")]
    public string FirstName { get; private init; }
}

You need the [JsonInclude] to allow the deserializer to use the private init.

This won't change the public api of the struct, which it seems from your question you might be okay with.

Prior to C# 9 you can use private set; but this opens the api internally a little more.

8 Comments

I am sorry, but I tested it and it is not working too
Have a look at this. What isn't working?
Don't forget to remove the [JsonConstructor] attribute
I am sorry but it works with struct but doesnt work with class. Could you post the solution for the class too, pls?
Ok, thanks. yes when I remove JsonConstructor and create public default constructor then it works. But IMHO in any case Text.Json offers an akward solution.
|
0

You can use a json constructor and use the property or field names as they are in reflection or use The JsonParameterName attribute and use no json constructor, you can't do both. Perhaps .net 9 will allow for it, but when you look at the source generator, it doesn't, as it uses reflection on the property names. To see for yourself, right-click ToDoJsonContext and select "go to definition." and look for how it expects the constructor to be called

As Yits mentions, remove the json constructor (meaning you need to support a parameterless constructor).

Here is a little test application:

using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace JsonTestConsole
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            /*test and work with a single entry*/

            var entry = new ToDo() { Rank = Random.Shared.Next(1, int.MaxValue), Task = "test a class" };
            var json = JsonSerializer.Serialize<ToDo>(entry, ToDoJsonContext.Default.ToDo);
            Console.WriteLine(json);
            Debug.Assert(json.Contains("\"a\":"), "supposed to have \"a\": property");
            var copy = JsonSerializer.Deserialize<ToDo>(json, ToDoJsonContext.Default.ToDo);
            Debug.Assert(copy is not null, "Serialization failed for a single ToDo");
            Debug.Assert(copy.Rank == entry.Rank, $"supposed to have the same rank {copy.Rank} and the original has {entry.Rank}.");
            Debug.Assert(string.Equals(copy.Task, entry.Task, StringComparison.Ordinal), $"Texts is supposed to be the same however copy task = {copy.Task} and entry task = {entry.Task}");
            Console.WriteLine($"Copy has Rank:{copy.Rank} and entry has {entry.Rank}");
            Console.WriteLine($"Copy has Task:{copy.Task} and entry has {entry.Task}");

            /*test and work with a list*/
            var data = new List<ToDo> { new() { Rank = 1, Task = "test json serialization" }, new() { Rank = 2, Task = "test call constructor" } };
            using var stream = new MemoryStream();
            await JsonSerializer.SerializeAsync(stream, data, ToDoListJsonContext.Default.ListToDo);
            json = System.Text.Encoding.UTF8.GetString(stream.ToArray());

            Console.WriteLine(json);
            Debug.Assert(json.Contains("\"a\":"), "supposed to have \"a\": property");

            var clone = JsonSerializer.Deserialize<List<ToDo>>(json, ToDoListJsonContext.Default.ListToDo);
            Debug.Assert(clone is not null, "Serialization failed");
            Debug.Assert(clone.Count == data.Count, $"supposed to have the same number of items, clone has {clone.Count} and data has {data.Count}.");
            Console.WriteLine($"Cone has {clone.Count} entries");
#if !DEBUG
            Console.WriteLine("Press enter to exit");
            Console.ReadLine();
#endif
        }
    }

    [System.Text.Json.Serialization.JsonSerializable(typeof(ToDo))]
    [System.Text.Json.Serialization.JsonSourceGenerationOptions(
            GenerationMode = System.Text.Json.Serialization.JsonSourceGenerationMode.Metadata)]
    public partial class ToDoJsonContext : System.Text.Json.Serialization.JsonSerializerContext
    {
    }

    [System.Text.Json.Serialization.JsonSerializable(typeof(List<ToDo>))]
    [System.Text.Json.Serialization.JsonSourceGenerationOptions(GenerationMode = System.Text.Json.Serialization.JsonSourceGenerationMode.Metadata)]
    public partial class ToDoListJsonContext : System.Text.Json.Serialization.JsonSerializerContext
    {
    }


    public class ToDo
    {


        [JsonInclude]
        [JsonPropertyName("a")]
        public int Rank { get; set; }

        [JsonInclude]
        [JsonPropertyName("b")]
        public required string Task { get; set; }
    }

}

I also set my project to support AOT:

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

I run it in release mode as well as in AoT using the following publish settings: enter image description here

Works like a charm:

{"a":378268935,"b":"test a class"} [{"a":1,"b":"test json serialization"},{"a":2,"b":"test call constructor"}] Press enter to exit

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.