Search code examples
c#jsonserializationdeserializationsystem.text.json

System.Text.Json dictionary deserialisation as constructor parameter with reference id


I am using .NET 8 and try to deserialize a class with a [JsonConstructor] that has a Dictionary<string,int> as parameter. But this always fails with this error:

Reference metadata is not supported when deserializing constructor parameters.

I think this is because of the reference id "$id" in the JSON:

  "MyClass": {
    "$id": "3",
    "$type": "MyClassType",
    "MyDictionary": {
      "$id": "4",
      "aaaa": 5661,
      "bbbbb": 5661
    }
  }

This is my class:

public Dictionary<string, int> MyDictionary{ get; set; } = new();

[JsonConstructor]
private MyClass(Dictionary<string, int> MyDictionary)
{
    this.MyDictionary= MyDictionary;
    //...doing some other setup stuff
}

Is there any way to handle this without do write a custom converter?

If I do not use:

ReferenceHandler = ReferenceHandler.Preserve

this part works, but for my project I need to keep references.


Solution

  • The exception error message is self-explanatory: ReferenceHandler.Preserve does not work for types with parameterized constructors. System.Text.Json will throw an exception if you try to use it with a type with a parameterized constructor.

    I cannot find any place in the documentation where this is stated precisely, however in Preserve references and handle circular references MSFT writes:

    This feature can't be used to preserve value types or immutable types. On deserialization, the instance of an immutable type is created after the entire payload is read. So it would be impossible to deserialize the same instance if a reference to it appears within the JSON payload.

    For value types, immutable types, and arrays, no reference metadata is serialized. On deserialization, an exception is thrown if $ref or $id is found. However, value types ignore $id (and $values in the case of collections) to make it possible to deserialize payloads that were serialized by using Newtonsoft.Json. ...

    While MSFT does not state this explicitly, it seems that any type with a parameterized constructor is considered to be immutable for the purposes of this limitation.

    As a workaround, you will need to modify your class to have a parameterless constructor. Since your type is not in fact immutable (as your MyDictionary property has a setter), you could do so by moving the doing some other setup stuff setup logic into an IJsonOnDeserialized.OnDeserialized callback, e.g. like so:

    public class MyClass : IJsonOnDeserialized
    {
        public Dictionary<string, int> MyDictionary { get; set; }
    
        public MyClass() : this(new()) { }
    
        public MyClass(Dictionary<string, int> MyDictionary)
        {
            this.MyDictionary= MyDictionary;
            DoSetup();
        }       
    
        private void DoSetup()
        {
            //...doing some other setup stuff
            // Since this is called after deserialization you can use the populated dictionary in your setup, e.g. like so:
            InitialDictionaryCount = MyDictionary.Count;
        }
    
        void IJsonOnDeserialized.OnDeserialized() => DoSetup();
        
        [JsonIgnore]
        public int InitialDictionaryCount { get; private set; }
    }
    

    Since OnDeserialized() will be called after the MyClass instance is completely populated, you will be able to use MyDictionary and any other needed properties for your setup.

    Demo fiddle here.