Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System.Text.Json POCOs with constructor deserialization doesn't support metadata reads ($ref) #73302

Closed
Kiritsu opened this issue Aug 3, 2022 · 2 comments

Comments

@Kiritsu
Copy link

Kiritsu commented Aug 3, 2022

Description

I'm trying to deserialize some JSON that references other objects but it fails to do so.

I have the following configuration (only for the ReferenceHandler)

internal static readonly JsonSerializerOptions DefaultOptions = new()
{
    WriteIndented = true,
    ReferenceHandler = ReferenceHandler.Preserve
};

But when trying to deserialize the JSON, it fails with the following exception:

System.NotSupportedException : Reference metadata is not supported when deserializing constructor parameters. See type 'NTMA.Domain.Synoptic'. The unsupported member type is located on type 'NTMA.Domain.Component'. Path: $.Synoptics.$values[0].Components.$ref | LineNumber: 47 | BytePositionInLine: 21.
  ----> System.NotSupportedException : Reference metadata is not supported when deserializing constructor parameters. See type 'NTMA.Domain.Synoptic'.

I assume this issue is caused for the same reason as #66604 which attempted to be fixed in #67183. I had the issue with other objects with the field $id causing the same exception, so I updated to 7.0.0-preview.6.22324.4 which fixed the issue.

Below is a working reproduction code.

Reproduction Steps

The following code breaks:

// See https://aka.ms/new-console-template for more information

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

var exampleA = new ExampleA("Xyz")
{
    Bs = new HashSet<ExampleB>()
};

exampleA.Bs.Add(new ExampleB("Abc")
{
    A = exampleA
});

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve
};

var json = JsonSerializer.Serialize(exampleA, options);
exampleA = JsonSerializer.Deserialize<ExampleA>(json, options);

Console.WriteLine("Done.");

public class ExampleA
{
    public ISet<ExampleB> Bs { get; set; }
    
    public string Test { get; set; }

    public ExampleA(string test)
    {
        Test = test;
    }
}

public class ExampleB
{
    public ExampleA A { get; set; }

    public string Test { get; set; }

    public ExampleB(string test)
    {
        Test = test;
    }
}

The following code works:

// See https://aka.ms/new-console-template for more information

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

var exampleA = new ExampleA
{
    Bs = new HashSet<ExampleB>(),
    Test = "Xyz"
};

exampleA.Bs.Add(new ExampleB
{
    A = exampleA,
    Test = "Abc"
});

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve
};

var json = JsonSerializer.Serialize(exampleA, options);
exampleA = JsonSerializer.Deserialize<ExampleA>(json, options);

Console.WriteLine("Done.");

public class ExampleA
{
    public ISet<ExampleB> Bs { get; set; }
    
    public string Test { get; set; }
}

public class ExampleB
{
    public ExampleA A { get; set; }

    public string Test { get; set; }
}

Expected behavior

Deserialization happens properly even when there's a constructor.

Actual behavior

Deserialization can't happen because it says it's not supported even though it should be?

Regression?

No response

Known Workarounds

The only workaround in my case is to remove the constructors of my classes and use init-only properties but I'd like to not do this.

Configuration

.NET 6 (6.0.302)
Windows 11 (Version 21H2 (OS Build 22000.795))
x64

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Aug 3, 2022
@ghost
Copy link

ghost commented Aug 3, 2022

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

I'm trying to deserialize some JSON that references other objects but it fails to do so.

I have the following configuration (only for the ReferenceHandler)

internal static readonly JsonSerializerOptions DefaultOptions = new()
{
    WriteIndented = true,
    ReferenceHandler = ReferenceHandler.Preserve
};

But when trying to deserialize the JSON, it fails with the following exception:

System.NotSupportedException : Reference metadata is not supported when deserializing constructor parameters. See type 'NTMA.Domain.Synoptic'. The unsupported member type is located on type 'NTMA.Domain.Component'. Path: $.Synoptics.$values[0].Components.$ref | LineNumber: 47 | BytePositionInLine: 21.
  ----> System.NotSupportedException : Reference metadata is not supported when deserializing constructor parameters. See type 'NTMA.Domain.Synoptic'.

I assume this issue is caused for the same reason as #66604 which attempted to be fixed in #67183. I had the issue with other objects with the field $id causing the same exception, so I updated to 7.0.0-preview.6.22324.4 which fixed the issue.

Below is a working reproduction code.

Reproduction Steps

The following code breaks:

// See https://aka.ms/new-console-template for more information

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

var exampleA = new ExampleA("Xyz")
{
    Bs = new HashSet<ExampleB>()
};

exampleA.Bs.Add(new ExampleB("Abc")
{
    A = exampleA
});

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve
};

var json = JsonSerializer.Serialize(exampleA, options);
exampleA = JsonSerializer.Deserialize<ExampleA>(json, options);

Console.WriteLine("Done.");

public class ExampleA
{
    public ISet<ExampleB> Bs { get; set; }
    
    public string Test { get; set; }

    public ExampleA(string test)
    {
        Test = test;
    }
}

public class ExampleB
{
    public ExampleA A { get; set; }

    public string Test { get; set; }

    public ExampleB(string test)
    {
        Test = test;
    }
}

The following code works:

// See https://aka.ms/new-console-template for more information

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

var exampleA = new ExampleA
{
    Bs = new HashSet<ExampleB>(),
    Test = "Xyz"
};

exampleA.Bs.Add(new ExampleB
{
    A = exampleA,
    Test = "Abc"
});

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve
};

var json = JsonSerializer.Serialize(exampleA, options);
exampleA = JsonSerializer.Deserialize<ExampleA>(json, options);

Console.WriteLine("Done.");

public class ExampleA
{
    public ISet<ExampleB> Bs { get; set; }
    
    public string Test { get; set; }
}

public class ExampleB
{
    public ExampleA A { get; set; }

    public string Test { get; set; }
}

Expected behavior

Deserialization happens properly even when there's a constructor.

Actual behavior

Deserialization can't happen because it says it's not supported even though it should be?

Regression?

No response

Known Workarounds

The only workaround in my case is to remove the constructors of my classes and use init-only properties but I'd like to not do this.

Configuration

.NET 6 (6.0.302)
Windows 11 (Version 21H2 (OS Build 22000.795))
x64

Other information

No response

Author: Kiritsu
Assignees: -
Labels:

area-System.Text.Json

Milestone: -

@eiriktsarpalis
Copy link
Member

This is a known limitation. Ultimately, it is not possible to implement correctly out of bootstrapping concerns: constructor parameters must be deserialized before the current value can be instantiated. As such, it becomes impossible to deserialize values like the following:

JsonSerializer.Deserialize<Person>("""{ "$id" : "1", "parent" : { "$ref" : "1"} }""");

public record Person(Person? parent);

Even though not all reference preservation scenaria contain cycles, it arguably is the raison d'etre of the feature. While we could try to make the feature smarter and only fail if cycles are detected, this would require substantially more state to track correctly and it would impact performance. Consequently, explicitly not supporting the feature at all for constructor deserialization is the right apprioach in my view. As you mention, the workaround is to simply refactor constructor parameters to be init or required properties.

@eiriktsarpalis eiriktsarpalis closed this as not planned Won't fix, can't repro, duplicate, stale Aug 3, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Aug 3, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Sep 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants