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 source generation and records from another assembly #62374

Closed
vsfeedback opened this issue Dec 3, 2021 · 6 comments · Fixed by #62668
Closed

System.Text.Json source generation and records from another assembly #62374

vsfeedback opened this issue Dec 3, 2021 · 6 comments · Fixed by #62668
Assignees
Milestone

Comments

@vsfeedback
Copy link

This issue has been moved from a ticket on Developer Community.


Hi,

I'm trying to switch from Newtonsoft to System.Text.Json with source generation.
I'm experiencing an issue with serialization of records in another assembly.
I created a simple project, I have a library with a single file containing the record (I don't use terse syntax to easily switch to and from class type)

using System;
using System.Runtime.CompilerServices;

namespace ClassLibrary1
{
    public record Person
    {
        public Person(string name) => Name = name;

        public string Name { get; }
    }
}

and an executable with the following code, using System.Text.Json source generation:

using System.Text.Json;
using System.Text.Json.Serialization;
using ClassLibrary1;
using static System.Console;


var context = new Context();
var value = new Person("class");
var serialized = JsonSerializer.Serialize(value, typeof(Person), context);
WriteLine(serialized);
var unserialized = (Person) JsonSerializer.Deserialize(serialized, typeof(Person), context);

[JsonSerializable(typeof(Person))]
partial class Context : JsonSerializerContext
{
}

The output of the WriteLine is

{}

and then deserialization fails with the following error as it does not find the name:

Unhandled exception. System.InvalidOperationException: Each parameter in the deserialization constructor on type 'ClassLibrary1.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

Switching the type of Person from record to class fixes the issue. Also having the record directly in the executable (in the same assembly where the source is generated) fixes the issue.

I tried to reproduce by the generated code of the record (IEquatable, ToString, ...) manually in the class, but I can't reproduce the record issue.

By the way, if I look at the generated code in my Visual Studio
image.png
If I have a look at the Person.g.cs
For serialization it seems to contains the right code:

        private static void PersonSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::ClassLibrary1.Person? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
            writer.WriteString(PropName_Name, value.Name);
        
            writer.WriteEndObject();
        }

But if I look at the disassembly of my executable (I'm using dotPeek), I see why my serialized object is empty

        private static void PersonSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::ClassLibrary1.Person? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
        
            writer.WriteEndObject();
        }

I attached the project, it's fairly simple.

Thank you


Original Comments

(no comments)


Original Solutions

(no solutions)

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Text.Json untriaged New issue has not been triaged by the area owner labels Dec 3, 2021
@ghost
Copy link

ghost commented Dec 3, 2021

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

Issue Details

This issue has been moved from a ticket on Developer Community.


Hi,

I'm trying to switch from Newtonsoft to System.Text.Json with source generation.
I'm experiencing an issue with serialization of records in another assembly.
I created a simple project, I have a library with a single file containing the record (I don't use terse syntax to easily switch to and from class type)

using System;
using System.Runtime.CompilerServices;

namespace ClassLibrary1
{
    public record Person
    {
        public Person(string name) => Name = name;

        public string Name { get; }
    }
}

and an executable with the following code, using System.Text.Json source generation:

using System.Text.Json;
using System.Text.Json.Serialization;
using ClassLibrary1;
using static System.Console;


var context = new Context();
var value = new Person("class");
var serialized = JsonSerializer.Serialize(value, typeof(Person), context);
WriteLine(serialized);
var unserialized = (Person) JsonSerializer.Deserialize(serialized, typeof(Person), context);

[JsonSerializable(typeof(Person))]
partial class Context : JsonSerializerContext
{
}

The output of the WriteLine is

{}

and then deserialization fails with the following error as it does not find the name:

Unhandled exception. System.InvalidOperationException: Each parameter in the deserialization constructor on type 'ClassLibrary1.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

Switching the type of Person from record to class fixes the issue. Also having the record directly in the executable (in the same assembly where the source is generated) fixes the issue.

I tried to reproduce by the generated code of the record (IEquatable, ToString, ...) manually in the class, but I can't reproduce the record issue.

By the way, if I look at the generated code in my Visual Studio
image.png
If I have a look at the Person.g.cs
For serialization it seems to contains the right code:

        private static void PersonSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::ClassLibrary1.Person? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
            writer.WriteString(PropName_Name, value.Name);
        
            writer.WriteEndObject();
        }

But if I look at the disassembly of my executable (I'm using dotPeek), I see why my serialized object is empty

        private static void PersonSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::ClassLibrary1.Person? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
        
            writer.WriteEndObject();
        }

I attached the project, it's fairly simple.

Thank you


Original Comments

(no comments)


Original Solutions

(no solutions)

Author: vsfeedback
Assignees: -
Labels:

area-System.Text.Json, untriaged

Milestone: -

@eiriktsarpalis
Copy link
Member

This seems to specifically impact record types specified in other assemblies. I'm not sure why the property ends up being skipped, the generated source appears to be identical for both classes and records:

// <auto-generated/>
#nullable enable
    partial class Context
    {
        private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::ClassLibrary1.Person>? _Person;
        public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::ClassLibrary1.Person> Person
        {
            get
            {
                if (_Person == null)
                {
                    global::System.Text.Json.Serialization.JsonConverter? customConverter;
                    if (Options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(global::ClassLibrary1.Person))) != null)
                    {
                        _Person = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo<global::ClassLibrary1.Person>(Options, customConverter);
                    }
                    else
                    {
                        global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::ClassLibrary1.Person> objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::ClassLibrary1.Person>()
                        {
                            ObjectCreator = null,
                            ObjectWithParameterizedConstructorCreator = static (args) => new global::ClassLibrary1.Person((global::System.String)args[0]),
                            PropertyMetadataInitializer = PersonPropInit,
                            ConstructorParameterMetadataInitializer = PersonCtorParamInit,
                            NumberHandling = default,
                            SerializeHandler = PersonSerializeHandler
                        };
            
                        _Person = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::ClassLibrary1.Person>(Options, objectInfo);
                    }
                }
        
                return _Person;
            }
        }
        
        private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] PersonPropInit(global::System.Text.Json.Serialization.JsonSerializerContext context)
        {
            global::Context jsonContext = (global::Context)context;
            global::System.Text.Json.JsonSerializerOptions options = context.Options;
        
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[1];
        
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.String> info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.String>()
            {
                IsProperty = true,
                IsPublic = true,
                IsVirtual = false,
                DeclaringType = typeof(global::ClassLibrary1.Person),
                PropertyTypeInfo = jsonContext.String,
                Converter = null,
                Getter = static (obj) => ((global::ClassLibrary1.Person)obj).Name!,
                Setter = static (obj, value) => ((global::ClassLibrary1.Person)obj).Name = value!,
                IgnoreCondition = null,
                HasJsonInclude = false,
                IsExtensionData = false,
                NumberHandling = default,
                PropertyName = "Name",
                JsonPropertyName = null
            };
        
            properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.String>(options, info0);
            
            return properties;
        }
        
        private static void PersonSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::ClassLibrary1.Person? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
            writer.WriteString(PropName_Name, value.Name);
        
            writer.WriteEndObject();
        }
        
        private static global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] PersonCtorParamInit()
        {
            global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] parameters = new global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[1];
            global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues info;
        
            info = new()
            {
                Name = "name",
                ParameterType = typeof(global::System.String),
                Position = 0,
                HasDefaultValue = false,
                DefaultValue = default(global::System.String)
            };
            parameters[0] = info;
        
            return parameters;
        }
    }

cc @layomia

@layomia
Copy link
Contributor

layomia commented Dec 10, 2021

This causes silent data loss and is a candidate for a 6.0 servicing fix.

@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Dec 10, 2021
@layomia layomia added this to the 6.0.x milestone Dec 10, 2021
@layomia layomia self-assigned this Dec 10, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Dec 11, 2021
@MarkusRodler
Copy link

I also ran into this issue. If I create a record in the base assembly everything works. If I copy that record to another assembly and reference it, it doesn't generate it correctly.

Maybe the output helps. This is my class from the base assembly:

public sealed record Meal2(Guid Id, Guid RecipeId, string RecipeTitle, DateTime MealDate);

And this is the copy in the referenced assembly:

public sealed record Meal(Guid Id, Guid RecipeId, string RecipeTitle, DateTime MealDate);

Output of Meal2 source generation

// <auto-generated/>
#nullable enable
    partial class MessageSerializerContext
    {
        private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::Dark.Model.Meal2>? _Meal2;
        public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::Dark.Model.Meal2> Meal2
        {
            get
            {
                if (_Meal2 == null)
                {
                    global::System.Text.Json.Serialization.JsonConverter? customConverter;
                    if (Options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(global::Dark.Model.Meal2))) != null)
                    {
                        _Meal2 = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo<global::Dark.Model.Meal2>(Options, customConverter);
                    }
                    else
                    {
                        global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::Dark.Model.Meal2> objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::Dark.Model.Meal2>()
                        {
                            ObjectCreator = null,
                            ObjectWithParameterizedConstructorCreator = static (args) => new global::Dark.Model.Meal2((global::System.Guid)args[0], (global::System.Guid)args[1], (global::System.String)args[2], (global::System.DateTime)args[3]),
                            PropertyMetadataInitializer = Meal2PropInit,
                            ConstructorParameterMetadataInitializer = Meal2CtorParamInit,
                            NumberHandling = default,
                            SerializeHandler = Meal2SerializeHandler
                        };
            
                        _Meal2 = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::Dark.Model.Meal2>(Options, objectInfo);
                    }
                }
        
                return _Meal2;
            }
        }
        
        private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] Meal2PropInit(global::System.Text.Json.Serialization.JsonSerializerContext context)
        {
            global::MessageSerializerContext jsonContext = (global::MessageSerializerContext)context;
            global::System.Text.Json.JsonSerializerOptions options = context.Options;
        
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[4];
        
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Guid> info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Guid>()
            {
                IsProperty = true,
                IsPublic = true,
                IsVirtual = false,
                DeclaringType = typeof(global::Dark.Model.Meal2),
                PropertyTypeInfo = jsonContext.Guid,
                Converter = null,
                Getter = static (obj) => ((global::Dark.Model.Meal2)obj).Id,
                Setter = static (obj, value) => throw new global::System.InvalidOperationException("Deserialization of init-only properties is currently not supported in source generation mode."),
                IgnoreCondition = null,
                HasJsonInclude = false,
                IsExtensionData = false,
                NumberHandling = default,
                PropertyName = "Id",
                JsonPropertyName = null
            };
        
            properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Guid>(options, info0);
            
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Guid> info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Guid>()
            {
                IsProperty = true,
                IsPublic = true,
                IsVirtual = false,
                DeclaringType = typeof(global::Dark.Model.Meal2),
                PropertyTypeInfo = jsonContext.Guid,
                Converter = null,
                Getter = static (obj) => ((global::Dark.Model.Meal2)obj).RecipeId,
                Setter = static (obj, value) => throw new global::System.InvalidOperationException("Deserialization of init-only properties is currently not supported in source generation mode."),
                IgnoreCondition = null,
                HasJsonInclude = false,
                IsExtensionData = false,
                NumberHandling = default,
                PropertyName = "RecipeId",
                JsonPropertyName = null
            };
        
            properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Guid>(options, info1);
            
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.String> info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.String>()
            {
                IsProperty = true,
                IsPublic = true,
                IsVirtual = false,
                DeclaringType = typeof(global::Dark.Model.Meal2),
                PropertyTypeInfo = jsonContext.String,
                Converter = null,
                Getter = static (obj) => ((global::Dark.Model.Meal2)obj).RecipeTitle!,
                Setter = static (obj, value) => throw new global::System.InvalidOperationException("Deserialization of init-only properties is currently not supported in source generation mode."),
                IgnoreCondition = null,
                HasJsonInclude = false,
                IsExtensionData = false,
                NumberHandling = default,
                PropertyName = "RecipeTitle",
                JsonPropertyName = null
            };
        
            properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.String>(options, info2);
            
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.DateTime> info3 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.DateTime>()
            {
                IsProperty = true,
                IsPublic = true,
                IsVirtual = false,
                DeclaringType = typeof(global::Dark.Model.Meal2),
                PropertyTypeInfo = jsonContext.DateTime,
                Converter = null,
                Getter = static (obj) => ((global::Dark.Model.Meal2)obj).MealDate,
                Setter = static (obj, value) => throw new global::System.InvalidOperationException("Deserialization of init-only properties is currently not supported in source generation mode."),
                IgnoreCondition = null,
                HasJsonInclude = false,
                IsExtensionData = false,
                NumberHandling = default,
                PropertyName = "MealDate",
                JsonPropertyName = null
            };
        
            properties[3] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.DateTime>(options, info3);
            
            return properties;
        }
        
        private static void Meal2SerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::Dark.Model.Meal2? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
            writer.WriteString(PropName_Id, value.Id);
            writer.WriteString(PropName_RecipeId, value.RecipeId);
            writer.WriteString(PropName_RecipeTitle, value.RecipeTitle);
            writer.WriteString(PropName_MealDate, value.MealDate);
        
            writer.WriteEndObject();
        }
        
        private static global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] Meal2CtorParamInit()
        {
            global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] parameters = new global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[4];
            global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues info;
        
            info = new()
            {
                Name = "Id",
                ParameterType = typeof(global::System.Guid),
                Position = 0,
                HasDefaultValue = false,
                DefaultValue = default(global::System.Guid)
            };
            parameters[0] = info;
        
            info = new()
            {
                Name = "RecipeId",
                ParameterType = typeof(global::System.Guid),
                Position = 1,
                HasDefaultValue = false,
                DefaultValue = default(global::System.Guid)
            };
            parameters[1] = info;
        
            info = new()
            {
                Name = "RecipeTitle",
                ParameterType = typeof(global::System.String),
                Position = 2,
                HasDefaultValue = false,
                DefaultValue = default(global::System.String)
            };
            parameters[2] = info;
        
            info = new()
            {
                Name = "MealDate",
                ParameterType = typeof(global::System.DateTime),
                Position = 3,
                HasDefaultValue = false,
                DefaultValue = default(global::System.DateTime)
            };
            parameters[3] = info;
        
            return parameters;
        }
    }

Wrong Output of Meal source generation

// <auto-generated/>
#nullable enable
    partial class MessageSerializerContext
    {
        private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::Dark.Model.Meal>? _Meal;
        public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::Dark.Model.Meal> Meal
        {
            get
            {
                if (_Meal == null)
                {
                    global::System.Text.Json.Serialization.JsonConverter? customConverter;
                    if (Options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(global::Dark.Model.Meal))) != null)
                    {
                        _Meal = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo<global::Dark.Model.Meal>(Options, customConverter);
                    }
                    else
                    {
                        global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::Dark.Model.Meal> objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::Dark.Model.Meal>()
                        {
                            ObjectCreator = null,
                            ObjectWithParameterizedConstructorCreator = static (args) => new global::Dark.Model.Meal((global::System.Guid)args[0], (global::System.Guid)args[1], (global::System.String)args[2], (global::System.DateTime)args[3]),
                            PropertyMetadataInitializer = MealPropInit,
                            ConstructorParameterMetadataInitializer = MealCtorParamInit,
                            NumberHandling = default,
                            SerializeHandler = MealSerializeHandler
                        };
            
                        _Meal = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::Dark.Model.Meal>(Options, objectInfo);
                    }
                }
        
                return _Meal;
            }
        }
        
        private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] MealPropInit(global::System.Text.Json.Serialization.JsonSerializerContext context)
        {
            global::MessageSerializerContext jsonContext = (global::MessageSerializerContext)context;
            global::System.Text.Json.JsonSerializerOptions options = context.Options;
        
            global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] properties = global::System.Array.Empty<global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo>();
        
            return properties;
        }
        
        private static void MealSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::Dark.Model.Meal? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
        
            writer.WriteEndObject();
        }
        
        private static global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] MealCtorParamInit()
        {
            global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] parameters = new global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[4];
            global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues info;
        
            info = new()
            {
                Name = "Id",
                ParameterType = typeof(global::System.Guid),
                Position = 0,
                HasDefaultValue = false,
                DefaultValue = default(global::System.Guid)
            };
            parameters[0] = info;
        
            info = new()
            {
                Name = "RecipeId",
                ParameterType = typeof(global::System.Guid),
                Position = 1,
                HasDefaultValue = false,
                DefaultValue = default(global::System.Guid)
            };
            parameters[1] = info;
        
            info = new()
            {
                Name = "RecipeTitle",
                ParameterType = typeof(global::System.String),
                Position = 2,
                HasDefaultValue = false,
                DefaultValue = default(global::System.String)
            };
            parameters[2] = info;
        
            info = new()
            {
                Name = "MealDate",
                ParameterType = typeof(global::System.DateTime),
                Position = 3,
                HasDefaultValue = false,
                DefaultValue = default(global::System.DateTime)
            };
            parameters[3] = info;
        
            return parameters;
        }
    }

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jan 6, 2022
@layomia
Copy link
Contributor

layomia commented Jan 6, 2022

Reopening for servicing consideration.

@layomia
Copy link
Contributor

layomia commented Jan 8, 2022

Fixed for 6.0 in #63454.

@layomia layomia closed this as completed Jan 8, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Feb 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants