Skip to content

Add JSON source-gen mode that emits serialization logic #53212

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

Merged
merged 9 commits into from
May 27, 2021

Conversation

layomia
Copy link
Contributor

@layomia layomia commented May 25, 2021

Contributes to #51945.
Contributes to #52279.

Users kick off source generation by providing a partial, derived context class; and indicate serializable types and run-time options (optional):

namespace System.Text.Json.SourceGeneration.Tests
{
    [JsonSerializerOptions(
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
        IgnoreReadOnlyProperties = true,
        IgnoreRuntimeCustomConverters = true,
        NamingPolicy = JsonKnownNamingPolicy.BuiltInCamelCase)]
    [JsonSerializable(typeof(JsonMessage), GenerationMode = JsonSourceGenerationMode.Serialization)]
    internal partial class JsonContext : JsonSerializerContext
    {
    }

    public class JsonMessage
    {
        public string Message { get; set; }
        public int Length => Message?.Length ?? 0; // Read-only property
    }
}
Generated code (click to view)
// <auto-generated/>

namespace System.Text.Json.SourceGeneration.Tests
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.Json.SourceGeneration", "6.0.0.0")]
    internal partial class JsonContext : global::System.Text.Json.Serialization.JsonSerializerContext
    {
        private static global::System.Text.Json.JsonSerializerOptions s_defaultOptions { get; } = new global::System.Text.Json.JsonSerializerOptions()
            {
                DefaultIgnoreCondition = global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault,
                IgnoreReadOnlyFields = false,
                IgnoreReadOnlyProperties = true,
                IncludeFields = false,
                WriteIndented = false,
                PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase
            };
    
        private static global::System.Text.Json.SourceGeneration.Tests.JsonContext s_defaultContext;
        public static global::System.Text.Json.SourceGeneration.Tests.JsonContext Default => s_defaultContext ??= new global::System.Text.Json.SourceGeneration.Tests.JsonContext(new global::System.Text.Json.JsonSerializerOptions(s_defaultOptions));
    
        private static global::System.Text.Json.JsonEncodedText messagePropName = global::System.Text.Json.JsonEncodedText.Encode("message");
        private static global::System.Text.Json.JsonEncodedText lengthPropName = global::System.Text.Json.JsonEncodedText.Encode("length");
    
        public JsonContext() : base(null, s_defaultOptions)
        {
        }
    
        public JsonContext(global::System.Text.Json.JsonSerializerOptions options) : base(options, s_defaultOptions)
        {
        }
    
        public override global::System.Text.Json.Serialization.Metadata.JsonTypeInfo GetTypeInfo(global::System.Type type)
        {
            if (type == typeof(global::System.Text.Json.SourceGeneration.Tests.JsonMessage))
            {
                return this.JsonMessage;
            }
    
            return null!;
        }

        private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage> _JsonMessage;
        public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage> JsonMessage
        {
            get
            {
                if (_JsonMessage == null)
                {
                    global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage> objectInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage>();
                    _JsonMessage = objectInfo;
    
                    global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.InitializeObjectInfo(
                        objectInfo,
                        Options,
                        createObjectFunc: static () => new global::System.Text.Json.SourceGeneration.Tests.JsonMessage(),
                        propInitFunc: null,
                        default,
                        serializeFunc: JsonMessageSerialize);
                }
    
                return _JsonMessage;
            }
        }
    
        private static void JsonMessageSerialize(global::System.Text.Json.Utf8JsonWriter writer, global::System.Text.Json.SourceGeneration.Tests.JsonMessage value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
    
            writer.WriteStartObject();

            if (value.Message != null)
            {
                writer.WriteString(messagePropName, value.Message);
            }
    
            writer.WriteEndObject();
        }
    }
}

To use generated serialization code:

Interacting with generated func directly:

using MemoryStream ms = new();
Utf8JsonWriter writer = new(ms);
JsonContext.Default.JsonMessage.Serialize!(writer, new JsonMessage { Message = "Hello, world!" });
writer.Flush();

Interacting via JsonSerializer:

JsonSerializer.Serialize(new JsonMessage { Message = "Hello, world!" }, JsonContext.Default.JsonMessage);

or

JsonSerializer.Serialize(new JsonMessage { Message = "Hello, world!" }, typeof(JsonMessage), JsonContext.Default);

FYI @pranavkm @SteveSandersonMS @terrajobst @ericstj @jkotas @stephentoub @davidfowl @SamMonoRT @CoffeeFlux

@layomia layomia added this to the 6.0.0 milestone May 25, 2021
@layomia layomia requested a review from jozkee as a code owner May 25, 2021 02:32
@layomia layomia self-assigned this May 25, 2021
@ghost
Copy link

ghost commented May 25, 2021

Tagging subscribers to this area: @eiriktsarpalis, @layomia
See info in area-owners.md if you want to be subscribed.

Issue Details

Contributes to #51945.

Users kick off source generation by providing a partial, derived context class; and indicate serializable types and run-time options (optional):

namespace System.Text.Json.SourceGeneration.Tests
{
    [JsonSerializerOptions(
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
        IgnoreReadOnlyProperties = true,
        IgnoreRuntimeCustomConverters = true,
        NamingPolicy = JsonKnownNamingPolicy.BuiltInCamelCase)]
    [JsonSerializable(typeof(JsonMessage), GenerationMode = JsonSourceGenerationMode.Serialization)]
    internal partial class JsonContext : JsonSerializerContext
    {
    }

    public class JsonMessage
    {
        public string Message { get; set; }
        public int Length => Message?.Length ?? 0; // Read-only property
    }
}
Generated code (click to view)
// <auto-generated/>

namespace System.Text.Json.SourceGeneration.Tests
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.Json.SourceGeneration", "6.0.0.0")]
    internal partial class JsonContext : global::System.Text.Json.Serialization.JsonSerializerContext
    {
        private static global::System.Text.Json.JsonSerializerOptions s_defaultOptions { get; } = new global::System.Text.Json.JsonSerializerOptions()
            {
                DefaultIgnoreCondition = global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault,
                IgnoreReadOnlyFields = false,
                IgnoreReadOnlyProperties = true,
                IncludeFields = false,
                WriteIndented = false,
                PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase
            };
    
        private static global::System.Text.Json.SourceGeneration.Tests.JsonContext s_defaultContext;
        public static global::System.Text.Json.SourceGeneration.Tests.JsonContext Default => s_defaultContext ??= new global::System.Text.Json.SourceGeneration.Tests.JsonContext(new global::System.Text.Json.JsonSerializerOptions(s_defaultOptions));
    
        private static global::System.Text.Json.JsonEncodedText messagePropName = global::System.Text.Json.JsonEncodedText.Encode("message");
        private static global::System.Text.Json.JsonEncodedText lengthPropName = global::System.Text.Json.JsonEncodedText.Encode("length");
    
        public JsonContext() : base(null, s_defaultOptions)
        {
        }
    
        public JsonContext(global::System.Text.Json.JsonSerializerOptions options) : base(options, s_defaultOptions)
        {
        }
    
        public override global::System.Text.Json.Serialization.Metadata.JsonTypeInfo GetTypeInfo(global::System.Type type)
        {
            if (type == typeof(global::System.Text.Json.SourceGeneration.Tests.JsonMessage))
            {
                return this.JsonMessage;
            }
    
            return null!;
        }

        private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage> _JsonMessage;
        public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage> JsonMessage
        {
            get
            {
                if (_JsonMessage == null)
                {
                    global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage> objectInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::System.Text.Json.SourceGeneration.Tests.JsonMessage>();
                    _JsonMessage = objectInfo;
    
                    global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.InitializeObjectInfo(
                        objectInfo,
                        Options,
                        createObjectFunc: static () => new global::System.Text.Json.SourceGeneration.Tests.JsonMessage(),
                        propInitFunc: null,
                        default,
                        serializeFunc: JsonMessageSerialize);
                }
    
                return _JsonMessage;
            }
        }
    
        private static void JsonMessageSerialize(global::System.Text.Json.Utf8JsonWriter writer, global::System.Text.Json.SourceGeneration.Tests.JsonMessage value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
    
            writer.WriteStartObject();

            if (value.Message != null)
            {
                writer.WriteString(messagePropName, value.Message);
            }
    
            writer.WriteEndObject();
        }
    }
}

To use generated serialization code:

Interacting with generated func directly:

using MemoryStream ms = new();
Utf8JsonWriter writer = new(ms);
JsonContext.Default.JsonMessage.Serialize!(writer, new JsonMessage { Message = "Hello, world!" });
writer.Flush();

Interacting via JsonSerializer:

JsonSerializer.Serialize(new JsonMessage { Message = "Hello, world!" }, JsonContext.Default.JsonMessage);

FYI @pranavkm @SteveSandersonMS @terrajobst @ericstj @jkotas @stephentoub @davidfowl @SamMonoRT @CoffeeFlux

Author: layomia
Assignees: layomia
Labels:

area-System.Text.Json

Milestone: 6.0.0

@ghost
Copy link

ghost commented May 25, 2021

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ericstj ericstj added the breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. label May 25, 2021
@ghost ghost added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label May 25, 2021
@@ -165,15 +171,19 @@ public void TypeDiscoveryWithRenamedAttribute()
using System.Text.Json.Serialization;
using ReferencedAssembly;

using @JsonSerializable = System.Runtime.Serialization.ContractNamespaceAttribute;
using @JsonSerializable = System.Runtime.Serialization.CollectionDataContractAttribute ;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use S.R.S.CollectionDataContractAttribute here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to this because, like JsonSerializableAttribute, it is applied to classes.

Copy link
Contributor Author

@layomia layomia May 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is modeling type aliasing. In this case, we have two attributes switching names. The test makes sure that our check for [JsonSerializable(Type)] guards against such tricks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. A comment here about that would be useful. Thanks

@layomia layomia force-pushed the GenSerializationLogic branch from 330722b to f855af8 Compare May 26, 2021 23:45
Copy link
Member

@ericstj ericstj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm. Let’s get this in so we can get a preview s out. Any additional feedback can be addressed in follow up prs.

@ericstj
Copy link
Member

ericstj commented May 27, 2021

All these PGO failures were caused by #53301, @AndyAyersMS and @BruceForstall have helped get this fixed, but we can't get rid of the badges without pushing new changes / resetting CI. Since we know this PR didn't introduce the PGO failures we can safely ignore those.

@layomia
Copy link
Contributor Author

layomia commented May 27, 2021

The Build Browser wasm Release AllSubsets_Mono failure is unrelated and might be fixed by #53280. cc @ericstj

@layomia layomia merged commit 6e5f722 into dotnet:main May 27, 2021
@layomia
Copy link
Contributor Author

layomia commented May 27, 2021

/backport to release/6.0-preview5

@github-actions
Copy link
Contributor

Started backporting to release/6.0-preview5: https://github.com/dotnet/runtime/actions/runs/881152471

@layomia
Copy link
Contributor Author

layomia commented Nov 2, 2021

These breaking changes have already hit customers & the APIs have changed further since then - dotnet/docs#26200.

@layomia layomia removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Nov 2, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. new-api-needs-documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants