StronglyTypedId icon indicating copy to clipboard operation
StronglyTypedId copied to clipboard

Specify converter for JsonSerializerContext

Open fleed opened this issue 1 year ago • 3 comments

When using version 1.0.0-beta08 of the library in a .net 8 app, if you specify a System.Text.Json.Serialization.JsonSerializerContext, the StronglyTypedId will be serialized as an empty object.

Definition:

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, Converters = [typeof(ProjectId.ProjectIdSystemTextJsonConverter)])]
[JsonSerializable(typeof(Project))]
internal partial class ProjectSourceContext : JsonSerializerContext
{
}

Ideal usage:

[Fact]
    public async Task TestProjectSerializationDefaultAsync()
    {
        var project = new Project
        {
            Id = new ProjectId(new Guid("00000000-0000-0000-0000-000000000001")),
            Name = "Project 1"
        };
        var serialized = JsonSerializer.Serialize(project, ProjectSourceContext.Default.Project);
        await Verify(serialized);
    }

Value of serialized string:

{"id":{},"name":"Project 1"}

I am currently using the following solution:

[Fact]
    public async Task TestProjectSerializationAsync()
    {
        var project = new Project
        {
            Id = new ProjectId(new Guid("00000000-0000-0000-0000-000000000001")),
            Name = "Project 1"
        };
        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        options.Converters.Add(new ProjectId.ProjectIdSystemTextJsonConverter());
        var context = new ProjectSourceContext(options);
        var serialized = JsonSerializer.Serialize(project, context.Project);
        await Verify(serialized);
    }

In this case, you will get the expected JSON: {"id":"00000000-0000-0000-0000-000000000001","name":"Project 1"}. If you comment out the line options.Converters.Add(), you will still receive the incorrect serialized string with an empty object as the id.

This is the ideal solution I'm looking for:

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, Converters = [typeof(ProjectId.ProjectIdSystemTextJsonConverter)])]
[JsonSerializable(typeof(Project))]
internal partial class ProjectSourceContext : JsonSerializerContext
{
}

but I'm receiving the following compilation error:

Error SYSLIB1220 : The 'JsonConverterAttribute' type 'TestApp.Model.ProjectId.ProjectIdSystemTextJsonConverter' specified on member 'TestApp.Model.ProjectSourceContext' is not a converter type or does not contain an accessible parameterless constructor.

Is there another way to specify the converter for the Default serializer context object, to avoid specifying the options everywhere?

fleed avatar Jul 05 '24 03:07 fleed

Hey, sorry for the delay, I managed to miss this... did you ever get to the bottom of the issue? I've just given it a try and I can see that it has the behaviour you describe, but I'm not sure why 🤔 It's as though the source generator just isn't paying any attention to the Converters attribute for some reason (annoyingly the docs on it are very light too).

EDIT: ~I can't reproduce the compiler error you're seeing at all though 🤔 Which .NET version are you using? Neither .NET 8 or .NET 9 give the compilation error for me.~

I stand corrected, I can reproduce the error, and that's likely the source of the issue. I haven't figured out why yet though...

andrewlock avatar Dec 22 '24 22:12 andrewlock

Ok, I've just figured it out.

Source generators can't see the output of other source generators. So the Json Source Generator can't see the the converter generated by StronglyTypedId, hence the slightly strange compilation error.

Unfortunately, currently, there's no workaround for this, other than having the IDs defined in a different project to the project in which you're creating your JsonSerializerContext I think 🙁 Yes, that kinda sucks, but it's a limitation in the compiler currently.

andrewlock avatar Dec 22 '24 23:12 andrewlock

One possible workaround is the approach I use in the tests:

https://github.com/andrewlock/StronglyTypedId/blob/2313453da5db3dced61bcb99b82e9443f7f22367/test/StronglyTypedIds.IntegrationTests/SystemTextJsonSerializerContext.cs#L18-L30

This doesn't help with the Default case, but it does mean you can do something like this:

[JsonSerializable(typeof(Project))]
internal partial class ProjectSourceContext : JsonSerializerContext
{
    internal static SystemTextJsonSerializerContext Custom
        => new(new JsonSerializerOptions
        {
            Converters =
            {
                new ProjectId.ProjectIdSystemTextJsonConverter(),
            }
        });
}

And then you can use it like this:

var serialized = JsonSerializer.Serialize(project, ProjectSourceContext.Custom.Project);

andrewlock avatar Dec 27 '24 21:12 andrewlock