From 6e5f722a9dab49f9626ea95326ef6e74129621ca Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Thu, 27 May 2021 02:38:01 -0400 Subject: [PATCH] Add JSON source-gen mode that emits serialization logic (#53212) * Add JSON source-gen mode that emits serialization logic * Fix System.Net.Http.Json test issues * Fix System.Text.Json test issues * Make check to determine if fast-path can be used more efficient * Address review feedback * Improve derived-JsonSerializerContext detection and support * Address review feedback; reenable tests, and simplify object metadata init * Fix formatting --- .../FunctionalTests/JsonContext/JsonContext.cs | 4 +- .../tests/FunctionalTests/JsonContext/Person.cs | 11 +- .../Attributes => Common}/JsonAttribute.cs | 7 +- .../JsonCamelCaseNamingPolicy.cs | 0 .../Common/JsonKnownNamingPolicy.cs | 26 + .../Serialization => Common}/JsonNamingPolicy.cs | 9 +- .../Common/JsonSerializerOptionsAttribute.cs | 53 + .../Common/JsonSourceGenerationMode.cs | 42 + .../System.Text.Json/gen/ContextGenerationSpec.cs | 37 + .../gen/JsonSourceGenerator.Emitter.cs | 1015 +++++++++++++------- .../gen/JsonSourceGenerator.Parser.cs | 413 ++++++-- .../System.Text.Json/gen/JsonSourceGenerator.cs | 38 +- ...opertyMetadata.cs => PropertyGenerationSpec.cs} | 19 +- .../gen/Reflection/TypeExtensions.cs | 24 +- .../System.Text.Json/gen/Resources/Strings.resx | 6 + .../gen/Resources/xlf/Strings.cs.xlf | 10 + .../gen/Resources/xlf/Strings.de.xlf | 10 + .../gen/Resources/xlf/Strings.es.xlf | 10 + .../gen/Resources/xlf/Strings.fr.xlf | 10 + .../gen/Resources/xlf/Strings.it.xlf | 10 + .../gen/Resources/xlf/Strings.ja.xlf | 10 + .../gen/Resources/xlf/Strings.ko.xlf | 10 + .../gen/Resources/xlf/Strings.pl.xlf | 10 + .../gen/Resources/xlf/Strings.pt-BR.xlf | 10 + .../gen/Resources/xlf/Strings.ru.xlf | 10 + .../gen/Resources/xlf/Strings.tr.xlf | 10 + .../gen/Resources/xlf/Strings.zh-Hans.xlf | 10 + .../gen/Resources/xlf/Strings.zh-Hant.xlf | 10 + .../System.Text.Json/gen/SourceGenerationSpec.cs | 29 + .../gen/System.Text.Json.SourceGeneration.csproj | 12 +- .../System.Text.Json/gen/TypeGenerationSpec.cs | 110 +++ src/libraries/System.Text.Json/gen/TypeMetadata.cs | 82 -- .../System.Text.Json/ref/System.Text.Json.cs | 39 +- .../System.Text.Json/src/Resources/Strings.resx | 9 + .../System.Text.Json/src/System.Text.Json.csproj | 14 +- .../Attributes/JsonSerializableAttribute.cs | 13 +- .../Converters/JsonMetadataServicesConverter.cs | 90 ++ .../Converters/Object/ObjectDefaultConverter.cs | 28 +- .../Converters/Object/ObjectSourceGenConverter.cs | 46 - .../Json/Serialization/JsonDefaultNamingPolicy.cs | 10 - .../Serialization/JsonResumableConverterOfT.cs | 2 - .../Serialization/JsonSerializer.Write.Helpers.cs | 21 +- .../Json/Serialization/JsonSerializerContext.cs | 56 +- .../Metadata/JsonMetadataServices.Collections.cs | 31 +- .../Metadata/JsonMetadataServices.Converters.cs | 6 +- .../Serialization/Metadata/JsonMetadataServices.cs | 67 +- .../Serialization/Metadata/JsonTypeInfo.Cache.cs | 16 +- .../Json/Serialization/Metadata/JsonTypeInfo.cs | 9 +- .../Metadata/JsonTypeInfoInternalOfT.cs | 83 +- .../Json/Serialization/Metadata/JsonTypeInfoOfT.cs | 6 + .../System/Text/Json/ThrowHelper.Serialization.cs | 17 + .../tests/Common/JsonTestHelper.cs | 71 ++ .../ContextClasses.cs | 91 ++ .../JsonSerializerContextTests.cs | 30 + .../JsonTestHelper.cs | 17 + .../MetadataAndSerializationContextTests.cs | 57 ++ .../MetadataContextTests.cs | 56 ++ .../MixedModeContextTests.cs | 169 ++++ ...eGeneratorTests.cs => RealWorldContextTests.cs} | 188 ++-- .../SerializationContextTests.cs | 284 ++++++ .../SerializationLogicTests.cs | 61 ++ .../System.Text.Json.SourceGeneration.Tests.csproj | 11 +- .../TestClasses.cs | 22 +- .../CompilationHelper.cs | 12 +- .../JsonSourceGeneratorDiagnosticsTests.cs | 2 - .../JsonSourceGeneratorTests.cs | 48 +- .../TypeWrapperTests.cs | 16 +- .../tests/System.Text.Json.Tests/JsonTestHelper.cs | 61 +- .../MetadataTests/JsonContext/Dictionary.cs | 2 +- .../MetadataTests/JsonContext/HighLowTemps.cs | 11 +- .../MetadataTests/JsonContext/JsonContext.cs | 4 +- .../MetadataTests/JsonContext/List.cs | 2 +- .../MetadataTests/JsonContext/StringArray.cs | 2 +- .../JsonContext/WeatherForecastWithPOCOs.cs | 11 +- .../MetadataTests.JsonMetadataServices.cs | 75 +- .../MetadataTests/MetadataTests.Options.cs | 6 +- .../System.Text.Json.Tests.csproj | 1 + 77 files changed, 2950 insertions(+), 990 deletions(-) rename src/libraries/System.Text.Json/{src/System/Text/Json/Serialization/Attributes => Common}/JsonAttribute.cs (70%) rename src/libraries/System.Text.Json/{src/System/Text/Json/Serialization => Common}/JsonCamelCaseNamingPolicy.cs (100%) create mode 100644 src/libraries/System.Text.Json/Common/JsonKnownNamingPolicy.cs rename src/libraries/System.Text.Json/{src/System/Text/Json/Serialization => Common}/JsonNamingPolicy.cs (88%) create mode 100644 src/libraries/System.Text.Json/Common/JsonSerializerOptionsAttribute.cs create mode 100644 src/libraries/System.Text.Json/Common/JsonSourceGenerationMode.cs create mode 100644 src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs rename src/libraries/System.Text.Json/gen/{PropertyMetadata.cs => PropertyGenerationSpec.cs} (80%) create mode 100644 src/libraries/System.Text.Json/gen/SourceGenerationSpec.cs create mode 100644 src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs delete mode 100644 src/libraries/System.Text.Json/gen/TypeMetadata.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectSourceGenConverter.cs delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDefaultNamingPolicy.cs create mode 100644 src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonTestHelper.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs rename src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/{JsonSourceGeneratorTests.cs => RealWorldContextTests.cs} (74%) create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationLogicTests.cs diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/JsonContext.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/JsonContext.cs index 452644e..56a4ff5 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/JsonContext.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/JsonContext.cs @@ -13,11 +13,11 @@ namespace System.Net.Http.Json.Functional.Tests private static JsonContext s_default; public static JsonContext Default => s_default ??= new JsonContext(new JsonSerializerOptions()); - public JsonContext() : base(null) + public JsonContext() : base(null, null) { } - public JsonContext(JsonSerializerOptions options) : base(options) + public JsonContext(JsonSerializerOptions options) : base(options, null) { } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs index f383d17..36785e9 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs @@ -23,15 +23,14 @@ namespace System.Net.Http.Json.Functional.Tests } else { - JsonTypeInfo objectInfo = JsonMetadataServices.CreateObjectInfo(); - _Person = objectInfo; - - JsonMetadataServices.InitializeObjectInfo( - objectInfo, + JsonTypeInfo objectInfo = JsonMetadataServices.CreateObjectInfo( Options, createObjectFunc: static () => new Person(), PersonPropInitFunc, - default); + default, + serializeFunc: null); + + _Person = objectInfo; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonAttribute.cs b/src/libraries/System.Text.Json/Common/JsonAttribute.cs similarity index 70% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonAttribute.cs rename to src/libraries/System.Text.Json/Common/JsonAttribute.cs index ca6c5ba..93ca20b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonAttribute.cs @@ -6,5 +6,10 @@ namespace System.Text.Json.Serialization /// /// The base class of serialization attributes. /// - public abstract class JsonAttribute : Attribute { } +#if BUILDING_SOURCE_GENERATOR + internal +#else + public +#endif + abstract class JsonAttribute : Attribute { } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCamelCaseNamingPolicy.cs b/src/libraries/System.Text.Json/Common/JsonCamelCaseNamingPolicy.cs similarity index 100% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonCamelCaseNamingPolicy.cs rename to src/libraries/System.Text.Json/Common/JsonCamelCaseNamingPolicy.cs diff --git a/src/libraries/System.Text.Json/Common/JsonKnownNamingPolicy.cs b/src/libraries/System.Text.Json/Common/JsonKnownNamingPolicy.cs new file mode 100644 index 0000000..9929071 --- /dev/null +++ b/src/libraries/System.Text.Json/Common/JsonKnownNamingPolicy.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// The to be used at run-time. + /// +#if BUILDING_SOURCE_GENERATOR + internal +#else + public +#endif + enum JsonKnownNamingPolicy + { + /// + /// Specifies that JSON property names should not be converted. + /// + Unspecified = 0, + + /// + /// Specifies that the built-in be used to convert JSON property names. + /// + BuiltInCamelCase = 1 + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs b/src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs similarity index 88% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs rename to src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs index 30971ae..ee0a86c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs +++ b/src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs @@ -6,7 +6,12 @@ namespace System.Text.Json /// /// Determines the naming policy used to convert a string-based name to another format, such as a camel-casing format. /// - public abstract class JsonNamingPolicy +#if BUILDING_SOURCE_GENERATOR + internal +#else + public +#endif + abstract class JsonNamingPolicy { /// /// Initializes a new instance of . @@ -18,8 +23,6 @@ namespace System.Text.Json /// public static JsonNamingPolicy CamelCase { get; } = new JsonCamelCaseNamingPolicy(); - internal static JsonNamingPolicy Default { get; } = new JsonDefaultNamingPolicy(); - /// /// When overridden in a derived class, converts the specified name according to the policy. /// diff --git a/src/libraries/System.Text.Json/Common/JsonSerializerOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSerializerOptionsAttribute.cs new file mode 100644 index 0000000..a1b4d09 --- /dev/null +++ b/src/libraries/System.Text.Json/Common/JsonSerializerOptionsAttribute.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Instructs the System.Text.Json source generator to assume the specified + /// options will be used at run-time via . + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +#if BUILDING_SOURCE_GENERATOR + internal +#else + public +#endif + class JsonSerializerOptionsAttribute : JsonAttribute + { + /// + /// Specifies the default ignore condition. + /// + public JsonIgnoreCondition DefaultIgnoreCondition { get; set; } + + /// + /// Specifies whether to ignore read-only fields. + /// + public bool IgnoreReadOnlyFields { get; set; } + + /// + /// Specifies whether to ignore read-only properties. + /// + public bool IgnoreReadOnlyProperties { get; set; } + + /// + /// Specifies whether to ignore custom converters provided at run-time. + /// + public bool IgnoreRuntimeCustomConverters { get; set; } + + /// + /// Specifies whether to include fields for serialization and deserialization. + /// + public bool IncludeFields { get; set; } + + /// + /// Specifies a built-in naming polices to convert JSON property names with. + /// + public JsonKnownNamingPolicy NamingPolicy { get; set; } + + /// + /// Specifies whether JSON output should be pretty-printed. + /// + public bool WriteIndented { get; set; } + } +} diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationMode.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationMode.cs new file mode 100644 index 0000000..b46292b --- /dev/null +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationMode.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// The generation mode for the System.Text.Json source generator. + /// + [Flags] +#if BUILDING_SOURCE_GENERATOR + internal +#else + public +#endif + enum JsonSourceGenerationMode + { + /// + /// Instructs the JSON source generator to generate serialization logic and type metadata to fallback to + /// when the run-time options are not compatible with the indicated . + /// + /// + /// This mode supports all features. + /// + MetadataAndSerialization = 0, + + /// + /// Instructs the JSON source generator to generate type-metadata initialization logic. + /// + /// + /// This mode supports all features. + /// + Metadata = 1, + + /// + /// Instructs the JSON source generator to generate serialization logic. + /// + /// + /// This mode supports only a subset of features. + /// + Serialization = 2 + } +} diff --git a/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs b/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs new file mode 100644 index 0000000..7ba239f --- /dev/null +++ b/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.Json.SourceGeneration.Reflection; + +namespace System.Text.Json.SourceGeneration +{ + /// + /// Represents the set of input types and options needed to provide an + /// implementation for a user-provided JsonSerializerContext-derived type. + /// + internal sealed class ContextGenerationSpec + { + public JsonSerializerOptionsAttribute SerializerOptions { get; init; } + + public Type ContextType { get; init; } + + public List? RootSerializableTypes { get; init; } + + public List ContextClassDeclarationList { get; init; } + + /// + /// Types that we have initiated serialization metadata generation for. A type may be discoverable in the object graph, + /// but not reachable for serialization (e.g. it is [JsonIgnore]'d); thus we maintain a separate cache. + /// + public HashSet TypesWithMetadataGenerated { get; } = new(); + + /// + /// Cache of runtime property names (statically determined) found accross the object graph of the JsonSerializerContext. + /// + public HashSet RuntimePropertyNames { get; } = new(); + + public string ContextTypeRef => $"global::{ContextType.GetUniqueCompilableTypeName()}"; + } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index a176de6..251aa2a 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; -using System.Text.Json.SourceGeneration.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; @@ -13,21 +12,48 @@ namespace System.Text.Json.SourceGeneration { public sealed partial class JsonSourceGenerator { - private sealed class Emitter + private sealed partial class Emitter { + // Literals in generated source private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter"; - - private const string JsonContextDeclarationSource = "internal partial class JsonContext : JsonSerializerContext"; - private const string OptionsInstanceVariableName = "Options"; - - private const string PropInitFuncVarName = "PropInitFunc"; - - private const string JsonMetadataServicesClassName = "JsonMetadataServices"; - + private const string PropInitMethodNameSuffix = "PropInit"; + private const string SerializeMethodNameSuffix = "Serialize"; private const string CreateValueInfoMethodName = "CreateValueInfo"; + private const string DefaultOptionsStaticVarName = "s_defaultOptions"; + private const string DefaultContextBackingStaticVarName = "s_defaultContext"; + private const string WriterVarName = "writer"; + private const string ValueVarName = "value"; + private const string JsonSerializerContextName = "JsonSerializerContext"; + + private static AssemblyName _assemblyName = typeof(Emitter).Assembly.GetName(); + private static readonly string s_generatedCodeAttributeSource = $@" +[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{_assemblyName.Name}"", ""{_assemblyName.Version}"")] +"; - private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration"; + // global::fully.qualified.name for referenced types + private const string ArrayTypeRef = "global::System.Array"; + private const string InvalidOperationExceptionTypeRef = "global::System.InvalidOperationException"; + private const string TypeTypeRef = "global::System.Type"; + private const string UnsafeTypeRef = "global::System.CompilerServices.Unsafe"; + private const string NullableTypeRef = "global::System.Nullable"; + private const string IListTypeRef = "global::System.Collections.Generic.IList"; + private const string KeyValuePairTypeRef = "global::System.Collections.Generic.KeyValuePair"; + private const string ListTypeRef = "global::System.Collections.Generic.List"; + private const string DictionaryTypeRef = "global::System.Collections.Generic.Dictionary"; + private const string JsonEncodedTextTypeRef = "global::System.Text.Json.JsonEncodedText"; + private const string JsonNamingPolicyTypeRef = "global::System.Text.Json.JsonNamingPolicy"; + private const string JsonSerializerTypeRef = "global::System.Text.Json.JsonSerializer"; + private const string JsonSerializerOptionsTypeRef = "global::System.Text.Json.JsonSerializerOptions"; + private const string Utf8JsonWriterTypeRef = "global::System.Text.Json.Utf8JsonWriter"; + private const string JsonConverterTypeRef = "global::System.Text.Json.Serialization.JsonConverter"; + private const string JsonConverterFactoryTypeRef = "global::System.Text.Json.Serialization.JsonConverterFactory"; + private const string JsonIgnoreConditionTypeRef = "global::System.Text.Json.Serialization.JsonIgnoreCondition"; + private const string JsonNumberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonNumberHandling"; + private const string JsonSerializerContextTypeRef = "global::System.Text.Json.Serialization.JsonSerializerContext"; + private const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices"; + private const string JsonPropertyInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo"; + private const string JsonTypeInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonTypeInfo"; private static DiagnosticDescriptor TypeNotSupported { get; } = new DiagnosticDescriptor( id: "SYSLIB1030", @@ -45,109 +71,150 @@ namespace System.Text.Json.SourceGeneration defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - private readonly string _generationNamespace; - - // TODO (https://github.com/dotnet/runtime/issues/52218): consider public option for this. - // Converter-honoring logic generation can be simplified - // if we don't plan to have a feature around this. - private readonly bool _honorRuntimeProvidedCustomConverters = true; - private readonly GeneratorExecutionContext _executionContext; - /// - /// Types that we have initiated serialization metadata generation for. A type may be discoverable in the object graph, - /// but not reachable for serialization (e.g. it is [JsonIgnore]'d); thus we maintain a separate cache. - /// - private readonly HashSet _typesWithMetadataGenerated = new(); + private ContextGenerationSpec _currentContext = null!; - /// - /// Types that were specified with System.Text.Json.Serialization.JsonSerializableAttribute. - /// - private readonly Dictionary _rootSerializableTypes = null!; + private readonly SourceGenerationSpec _generationSpec = null!; - public Emitter(in GeneratorExecutionContext executionContext, Dictionary rootSerializableTypes) + public Emitter(in GeneratorExecutionContext executionContext, SourceGenerationSpec generationSpec) { _executionContext = executionContext; - _generationNamespace = $"{executionContext.Compilation.AssemblyName}.JsonSourceGeneration"; - _rootSerializableTypes = rootSerializableTypes; + _generationSpec = generationSpec; } public void Emit() { - foreach (KeyValuePair pair in _rootSerializableTypes) + foreach (ContextGenerationSpec contextGenerationSpec in _generationSpec.ContextGenerationSpecList) { - TypeMetadata typeMetadata = pair.Value; - GenerateTypeMetadata(typeMetadata); + _currentContext = contextGenerationSpec; + + foreach (TypeGenerationSpec typeGenerationSpec in _currentContext.RootSerializableTypes) + { + GenerateTypeInfo(typeGenerationSpec); + } + + string contextName = _currentContext.ContextType.Name; + + // Add root context implementation. + AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(), isRootContextDef: true); + + // Add GetJsonTypeInfo override implementation. + AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation()); + + // Add property name initialization. + AddSource($"{contextName}.PropertyNames.g.cs", GetPropertyNameInitialization()); } + } + + private void AddSource(string fileName, string source, bool isRootContextDef = false) + { + string? generatedCodeAttributeSource = isRootContextDef ? s_generatedCodeAttributeSource : null; + + List declarationList = _currentContext.ContextClassDeclarationList; + int declarationCount = declarationList.Count; + Debug.Assert(declarationCount >= 1); + + StringBuilder sb = new(); - // Add base default instance source. - _executionContext.AddSource("JsonContext.g.cs", SourceText.From(GetBaseJsonContextImplementation(), Encoding.UTF8)); + sb.Append($@"// - // Add GetJsonTypeInfo override implementation. - _executionContext.AddSource("JsonContext.GetJsonTypeInfo.g.cs", SourceText.From(GetGetTypeInfoImplementation(), Encoding.UTF8)); +namespace {_currentContext.ContextType.Namespace} +{{"); + + for (int i = 0; i < declarationCount - 1; i++) + { + string declarationSource = $@" +{declarationList[declarationCount - 1 - i]} +{{"; + sb.Append($@" +{IndentSource(declarationSource, numIndentations: i + 1)} +"); + } + + // Add the core implementation for the derived context class. + string partialContextImplementation = $@" +{generatedCodeAttributeSource}{declarationList[0]} +{{ + {IndentSource(source, Math.Max(1, declarationCount - 1))} +}}"; + sb.AppendLine(IndentSource(partialContextImplementation, numIndentations: declarationCount)); + + // Match curly brace for each containing type. + for (int i = 0; i < declarationCount - 1; i++) + { + sb.AppendLine(IndentSource("}", numIndentations: declarationCount + i + 1)); + } + + // Match curly brace for namespace. + sb.AppendLine("}"); + + _executionContext.AddSource(fileName, SourceText.From(sb.ToString(), Encoding.UTF8)); } - private void GenerateTypeMetadata(TypeMetadata typeMetadata) + private void GenerateTypeInfo(TypeGenerationSpec typeGenerationSpec) { - Debug.Assert(typeMetadata != null); + Debug.Assert(typeGenerationSpec != null); + + HashSet typesWithMetadata = _currentContext.TypesWithMetadataGenerated; - if (_typesWithMetadataGenerated.Contains(typeMetadata)) + if (typesWithMetadata.Contains(typeGenerationSpec)) { return; } - _typesWithMetadataGenerated.Add(typeMetadata); + typesWithMetadata.Add(typeGenerationSpec); string source; - switch (typeMetadata.ClassType) + switch (typeGenerationSpec.ClassType) { case ClassType.KnownType: { - source = GenerateForTypeWithKnownConverter(typeMetadata); + source = GenerateForTypeWithKnownConverter(typeGenerationSpec); } break; case ClassType.TypeWithDesignTimeProvidedCustomConverter: { - source = GenerateForTypeWithUnknownConverter(typeMetadata); + source = GenerateForTypeWithUnknownConverter(typeGenerationSpec); } break; case ClassType.Nullable: { - source = GenerateForNullable(typeMetadata); + source = GenerateForNullable(typeGenerationSpec); - GenerateTypeMetadata(typeMetadata.NullableUnderlyingTypeMetadata); + GenerateTypeInfo(typeGenerationSpec.NullableUnderlyingTypeMetadata); } break; case ClassType.Enum: { - source = GenerateForEnum(typeMetadata); + source = GenerateForEnum(typeGenerationSpec); } break; case ClassType.Enumerable: { - source = GenerateForCollection(typeMetadata); + source = GenerateForCollection(typeGenerationSpec); - GenerateTypeMetadata(typeMetadata.CollectionValueTypeMetadata); + GenerateTypeInfo(typeGenerationSpec.CollectionValueTypeMetadata); } break; case ClassType.Dictionary: { - source = GenerateForCollection(typeMetadata); + source = GenerateForCollection(typeGenerationSpec); - GenerateTypeMetadata(typeMetadata.CollectionKeyTypeMetadata); - GenerateTypeMetadata(typeMetadata.CollectionValueTypeMetadata); + GenerateTypeInfo(typeGenerationSpec.CollectionKeyTypeMetadata); + GenerateTypeInfo(typeGenerationSpec.CollectionValueTypeMetadata); } break; case ClassType.Object: { - source = GenerateForObject(typeMetadata); + source = GenerateForObject(typeGenerationSpec); - if (typeMetadata.PropertiesMetadata != null) + if (typeGenerationSpec.PropertiesMetadata != null) { - foreach (PropertyMetadata metadata in typeMetadata.PropertiesMetadata) + foreach (PropertyGenerationSpec metadata in typeGenerationSpec.PropertiesMetadata) { - GenerateTypeMetadata(metadata.TypeMetadata); + GenerateTypeInfo(metadata.TypeGenerationSpec); } } } @@ -155,7 +222,7 @@ namespace System.Text.Json.SourceGeneration case ClassType.TypeUnsupportedBySourceGen: { _executionContext.ReportDiagnostic( - Diagnostic.Create(TypeNotSupported, Location.None, new string[] { typeMetadata.CompilableName })); + Diagnostic.Create(TypeNotSupported, Location.None, new string[] { typeGenerationSpec.TypeRef })); return; } default: @@ -166,109 +233,109 @@ namespace System.Text.Json.SourceGeneration try { - _executionContext.AddSource($"{typeMetadata.FriendlyName}.cs", SourceText.From(source, Encoding.UTF8)); + AddSource($"{_currentContext.ContextType.Name}.{typeGenerationSpec.TypeInfoPropertyName}.g.cs", source); } catch (ArgumentException) { - _executionContext.ReportDiagnostic(Diagnostic.Create(DuplicateTypeName, Location.None, new string[] { typeMetadata.FriendlyName })); + _executionContext.ReportDiagnostic(Diagnostic.Create(DuplicateTypeName, Location.None, new string[] { typeGenerationSpec.TypeInfoPropertyName })); } } - private string GenerateForTypeWithKnownConverter(TypeMetadata typeMetadata) + private string GenerateForTypeWithKnownConverter(TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.CompilableName; - string typeFriendlyName = typeMetadata.FriendlyName; + string typeCompilableName = typeMetadata.TypeRef; + string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - string metadataInitSource = $@"_{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, {JsonMetadataServicesClassName}.{typeFriendlyName}Converter);"; + string metadataInitSource = $@"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, {JsonMetadataServicesTypeRef}.{typeFriendlyName}Converter);"; return GenerateForType(typeMetadata, metadataInitSource); } - private string GenerateForTypeWithUnknownConverter(TypeMetadata typeMetadata) + private string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.CompilableName; - string typeFriendlyName = typeMetadata.FriendlyName; + string typeCompilableName = typeMetadata.TypeRef; + string typeFriendlyName = typeMetadata.TypeInfoPropertyName; StringBuilder sb = new(); // TODO (https://github.com/dotnet/runtime/issues/52218): consider moving this verification source to common helper. - string metadataInitSource = $@"JsonConverter converter = {typeMetadata.ConverterInstantiationLogic}; - Type typeToConvert = typeof({typeCompilableName}); + string metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; + {TypeTypeRef} typeToConvert = typeof({typeCompilableName}); if (!converter.CanConvert(typeToConvert)) {{ - Type underlyingType = Nullable.GetUnderlyingType(typeToConvert); + {TypeTypeRef} underlyingType = {NullableTypeRef}.GetUnderlyingType(typeToConvert); if (underlyingType != null && converter.CanConvert(underlyingType)) {{ - JsonConverter actualConverter = converter; + {JsonConverterTypeRef} actualConverter = converter; - if (converter is JsonConverterFactory converterFactory) + if (converter is {JsonConverterFactoryTypeRef} converterFactory) {{ actualConverter = converterFactory.CreateConverter(underlyingType, {OptionsInstanceVariableName}); - if (actualConverter == null || actualConverter is JsonConverterFactory) + if (actualConverter == null || actualConverter is {JsonConverterFactoryTypeRef}) {{ - throw new InvalidOperationException($""JsonConverterFactory '{{converter}} cannot return a 'null' or 'JsonConverterFactory' value.""); + throw new {InvalidOperationExceptionTypeRef}($""JsonConverterFactory '{{converter}} cannot return a 'null' or 'JsonConverterFactory' value.""); }} }} // Allow nullable handling to forward to the underlying type's converter. - converter = {JsonMetadataServicesClassName}.GetNullableConverter<{typeCompilableName}>((JsonConverter<{typeCompilableName}>)actualConverter); + converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(({JsonConverterTypeRef}<{typeCompilableName}>)actualConverter); }} else {{ - throw new InvalidOperationException($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); + throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); }} }} - _{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);"; + _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);"; return GenerateForType(typeMetadata, metadataInitSource); } - private string GenerateForNullable(TypeMetadata typeMetadata) + private string GenerateForNullable(TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.CompilableName; - string typeFriendlyName = typeMetadata.FriendlyName; + string typeCompilableName = typeMetadata.TypeRef; + string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - TypeMetadata? underlyingTypeMetadata = typeMetadata.NullableUnderlyingTypeMetadata; + TypeGenerationSpec? underlyingTypeMetadata = typeMetadata.NullableUnderlyingTypeMetadata; Debug.Assert(underlyingTypeMetadata != null); - string underlyingTypeCompilableName = underlyingTypeMetadata.CompilableName; - string underlyingTypeFriendlyName = underlyingTypeMetadata.FriendlyName; + string underlyingTypeCompilableName = underlyingTypeMetadata.TypeRef; + string underlyingTypeFriendlyName = underlyingTypeMetadata.TypeInfoPropertyName; string underlyingTypeInfoNamedArg = underlyingTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen ? "underlyingTypeInfo: null" : $"underlyingTypeInfo: {underlyingTypeFriendlyName}"; - string metadataInitSource = @$"_{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}( + string metadataInitSource = @$"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}( {OptionsInstanceVariableName}, - {JsonMetadataServicesClassName}.GetNullableConverter<{underlyingTypeCompilableName}>({underlyingTypeInfoNamedArg})); + {JsonMetadataServicesTypeRef}.GetNullableConverter<{underlyingTypeCompilableName}>({underlyingTypeInfoNamedArg})); "; return GenerateForType(typeMetadata, metadataInitSource); } - private string GenerateForEnum(TypeMetadata typeMetadata) + private string GenerateForEnum(TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.CompilableName; - string typeFriendlyName = typeMetadata.FriendlyName; + string typeCompilableName = typeMetadata.TypeRef; + string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - string metadataInitSource = $"_{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, JsonMetadataServices.GetEnumConverter<{typeCompilableName}>({OptionsInstanceVariableName}));"; + string metadataInitSource = $"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeCompilableName}>({OptionsInstanceVariableName}));"; return GenerateForType(typeMetadata, metadataInitSource); } - private string GenerateForCollection(TypeMetadata typeMetadata) + private string GenerateForCollection(TypeGenerationSpec typeGenerationSpec) { - string typeCompilableName = typeMetadata.CompilableName; - string typeFriendlyName = typeMetadata.FriendlyName; + string typeCompilableName = typeGenerationSpec.TypeRef; + string typeFriendlyName = typeGenerationSpec.TypeInfoPropertyName; // Key metadata - TypeMetadata? collectionKeyTypeMetadata = typeMetadata.CollectionKeyTypeMetadata; - Debug.Assert(!(typeMetadata.CollectionType == CollectionType.Dictionary && collectionKeyTypeMetadata == null)); - string? keyTypeCompilableName = collectionKeyTypeMetadata?.CompilableName; - string? keyTypeReadableName = collectionKeyTypeMetadata?.FriendlyName; + TypeGenerationSpec? collectionKeyTypeMetadata = typeGenerationSpec.CollectionKeyTypeMetadata; + Debug.Assert(!(typeGenerationSpec.CollectionType == CollectionType.Dictionary && collectionKeyTypeMetadata == null)); + string? keyTypeCompilableName = collectionKeyTypeMetadata?.TypeRef; + string? keyTypeReadableName = collectionKeyTypeMetadata?.TypeInfoPropertyName; string? keyTypeMetadataPropertyName; - if (typeMetadata.ClassType != ClassType.Dictionary) + if (typeGenerationSpec.ClassType != ClassType.Dictionary) { keyTypeMetadataPropertyName = "null"; } @@ -280,102 +347,230 @@ namespace System.Text.Json.SourceGeneration } // Value metadata - TypeMetadata? collectionValueTypeMetadata = typeMetadata.CollectionValueTypeMetadata; + TypeGenerationSpec? collectionValueTypeMetadata = typeGenerationSpec.CollectionValueTypeMetadata; Debug.Assert(collectionValueTypeMetadata != null); - string valueTypeCompilableName = collectionValueTypeMetadata.CompilableName; - string valueTypeReadableName = collectionValueTypeMetadata.FriendlyName; + string valueTypeCompilableName = collectionValueTypeMetadata.TypeRef; + string valueTypeReadableName = collectionValueTypeMetadata.TypeInfoPropertyName; string valueTypeMetadataPropertyName = collectionValueTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen ? "null" : $"this.{valueTypeReadableName}"; - string numberHandlingArg = $"{GetNumberHandlingAsStr(typeMetadata.NumberHandling)}"; + string numberHandlingArg = $"{GetNumberHandlingAsStr(typeGenerationSpec.NumberHandling)}"; + + string serializeMethodName = $"{typeFriendlyName}{SerializeMethodNameSuffix}"; + string serializeFuncNamedArg; + + CollectionType collectionType = typeGenerationSpec.CollectionType; + + string? serializeFuncSource; + if (!typeGenerationSpec.GenerateSerializationLogic) + { + serializeFuncSource = null; + serializeFuncNamedArg = "serializeFunc: null"; + } + else + { + bool canBeNull = typeGenerationSpec.CanBeNull; + + switch (collectionType) + { + case CollectionType.Array: + serializeFuncSource = GenerateFastPathFuncForEnumerable(typeCompilableName, serializeMethodName, canBeNull, isArray: true, collectionValueTypeMetadata); + break; + case CollectionType.List: + serializeFuncSource = GenerateFastPathFuncForEnumerable(typeCompilableName, serializeMethodName, canBeNull, isArray: false, collectionValueTypeMetadata); + break; + case CollectionType.Dictionary: + serializeFuncSource = GenerateFastPathFuncForDictionary(typeCompilableName, serializeMethodName, canBeNull, collectionKeyTypeMetadata, collectionValueTypeMetadata); + break; + default: + serializeFuncSource = null; + break; + } + + serializeFuncNamedArg = $"serializeFunc: {serializeMethodName}"; + } - CollectionType collectionType = typeMetadata.CollectionType; string collectionTypeInfoValue = collectionType switch { - CollectionType.Array => $"{JsonMetadataServicesClassName}.CreateArrayInfo<{valueTypeCompilableName}>({OptionsInstanceVariableName}, {valueTypeMetadataPropertyName}, {numberHandlingArg})", - CollectionType.List => $"{JsonMetadataServicesClassName}.CreateListInfo<{typeCompilableName}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, () => new System.Collections.Generic.List<{valueTypeCompilableName}>(), {valueTypeMetadataPropertyName}, {numberHandlingArg})", - CollectionType.Dictionary => $"{JsonMetadataServicesClassName}.CreateDictionaryInfo<{typeCompilableName}, {keyTypeCompilableName!}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, () => new System.Collections.Generic.Dictionary<{keyTypeCompilableName}, {valueTypeCompilableName}>(), {keyTypeMetadataPropertyName!}, {valueTypeMetadataPropertyName}, {numberHandlingArg})", + CollectionType.Array => $"{JsonMetadataServicesTypeRef}.CreateArrayInfo<{valueTypeCompilableName}>({OptionsInstanceVariableName}, {valueTypeMetadataPropertyName}, {numberHandlingArg}, {serializeFuncNamedArg})", + CollectionType.List => $"{JsonMetadataServicesTypeRef}.CreateListInfo<{typeCompilableName}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, () => new {ListTypeRef}<{valueTypeCompilableName}>(), {valueTypeMetadataPropertyName}, {numberHandlingArg}, {serializeFuncNamedArg})", + CollectionType.Dictionary => $"{JsonMetadataServicesTypeRef}.CreateDictionaryInfo<{typeCompilableName}, {keyTypeCompilableName!}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, () => new {DictionaryTypeRef}<{keyTypeCompilableName}, {valueTypeCompilableName}>(), {keyTypeMetadataPropertyName!}, {valueTypeMetadataPropertyName}, {numberHandlingArg}, {serializeFuncNamedArg})", _ => throw new NotSupportedException() }; string metadataInitSource = @$"_{typeFriendlyName} = {collectionTypeInfoValue};"; - return GenerateForType(typeMetadata, metadataInitSource); + + return GenerateForType(typeGenerationSpec, metadataInitSource, serializeFuncSource); + } + + private string GenerateFastPathFuncForEnumerable(string typeInfoRef, string serializeMethodName, bool canBeNull, bool isArray, TypeGenerationSpec valueTypeGenerationSpec) + { + string? writerMethodToCall = GetWriterMethod(valueTypeGenerationSpec.Type); + string valueToWrite = $"{ValueVarName}[i]"; + string lengthPropName = isArray ? "Length" : "Count"; + + string elementSerializationLogic; + if (writerMethodToCall != null) + { + elementSerializationLogic = $"{writerMethodToCall}Value({valueToWrite});"; + } + else + { + elementSerializationLogic = GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec.TypeInfoPropertyName, valueToWrite, valueTypeGenerationSpec.GenerateSerializationLogic); + } + + string serializationLogic = $@"{WriterVarName}.WriteStartArray(); + + for (int i = 0; i < {ValueVarName}.{lengthPropName}; i++) + {{ + {elementSerializationLogic} + }} + + {WriterVarName}.WriteEndArray();"; + + return GenerateFastPathFuncForType(serializeMethodName, typeInfoRef, serializationLogic, canBeNull); } - private string GenerateForObject(TypeMetadata typeMetadata) + private string GenerateFastPathFuncForDictionary( + string typeInfoRef, + string serializeMethodName, + bool canBeNull, + TypeGenerationSpec keyTypeGenerationSpec, + TypeGenerationSpec valueTypeGenerationSpec) { - string typeCompilableName = typeMetadata.CompilableName; - string typeFriendlyName = typeMetadata.FriendlyName; + const string pairVarName = "pair"; + string keyToWrite = $"{pairVarName}.Key"; + string valueToWrite = $"{pairVarName}.Value"; + + string? writerMethodToCall = GetWriterMethod(valueTypeGenerationSpec.Type); + string elementSerializationLogic; + + if (writerMethodToCall != null) + { + elementSerializationLogic = $"{writerMethodToCall}({keyToWrite}, {valueToWrite});"; + } + else + { + elementSerializationLogic = $@"{WriterVarName}.WritePropertyName({keyToWrite}); + {GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec.TypeInfoPropertyName, valueToWrite, valueTypeGenerationSpec.GenerateSerializationLogic)}"; + } + + string serializationLogic = $@"{WriterVarName}.WriteStartObject(); + + foreach ({KeyValuePairTypeRef}<{keyTypeGenerationSpec.TypeRef}, {valueTypeGenerationSpec.TypeRef}> {pairVarName} in {ValueVarName}) + {{ + {elementSerializationLogic} + }} + + {WriterVarName}.WriteEndObject();"; + + return GenerateFastPathFuncForType(serializeMethodName, typeInfoRef, serializationLogic, canBeNull); + } + + private string GenerateForObject(TypeGenerationSpec typeMetadata) + { + string typeCompilableName = typeMetadata.TypeRef; + string typeFriendlyName = typeMetadata.TypeInfoPropertyName; string createObjectFuncTypeArg = typeMetadata.ConstructionStrategy == ObjectConstructionStrategy.ParameterlessConstructor - ? $"createObjectFunc: static () => new {typeMetadata.CompilableName}()" + ? $"createObjectFunc: static () => new {typeMetadata.TypeRef}()" : "createObjectFunc: null"; - List? properties = typeMetadata.PropertiesMetadata; + string propInitMethodName = $"{typeFriendlyName}{PropInitMethodNameSuffix}"; + string? propMetadataInitFuncSource = null; + string propMetadataInitFuncNamedArg; - StringBuilder sb = new(); + string serializeMethodName = $"{typeFriendlyName}{SerializeMethodNameSuffix}"; + string? serializeFuncSource = null; + string serializeFuncNamedArg; - sb.Append($@"JsonTypeInfo<{typeCompilableName}> objectInfo = {JsonMetadataServicesClassName}.CreateObjectInfo<{typeCompilableName}>(); - _{typeFriendlyName} = objectInfo; -"); + List? properties = typeMetadata.PropertiesMetadata; - string propInitFuncVarName = $"{typeFriendlyName}{PropInitFuncVarName}"; + if (typeMetadata.GenerateMetadata) + { + propMetadataInitFuncSource = GeneratePropMetadataInitFunc(typeMetadata.IsValueType, propInitMethodName, properties); + propMetadataInitFuncNamedArg = $@"propInitFunc: {propInitMethodName}"; + } + else + { + propMetadataInitFuncNamedArg = @"propInitFunc: null"; + } - sb.Append($@" - {JsonMetadataServicesClassName}.InitializeObjectInfo( - objectInfo, - {OptionsInstanceVariableName}, - {createObjectFuncTypeArg}, - {propInitFuncVarName}, - {GetNumberHandlingAsStr(typeMetadata.NumberHandling)});"); + if (typeMetadata.GenerateSerializationLogic) + { + serializeFuncSource = GenerateFastPathFuncForObject(typeCompilableName, serializeMethodName, typeMetadata.CanBeNull, properties); + serializeFuncNamedArg = $@"serializeFunc: {serializeMethodName}"; + } + else + { + serializeFuncNamedArg = @"serializeFunc: null"; + } + + string objectInfoInitSource = $@"{JsonTypeInfoTypeRef}<{typeCompilableName}> objectInfo = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeCompilableName}>( + {OptionsInstanceVariableName}, + {createObjectFuncTypeArg}, + {propMetadataInitFuncNamedArg}, + {GetNumberHandlingAsStr(typeMetadata.NumberHandling)}, + {serializeFuncNamedArg}); - string metadataInitSource = sb.ToString(); - string? propInitFuncSource = GeneratePropMetadataInitFunc(typeMetadata.IsValueType, propInitFuncVarName, properties); + _{typeFriendlyName} = objectInfo;"; - return GenerateForType(typeMetadata, metadataInitSource, propInitFuncSource); + string additionalSource; + if (propMetadataInitFuncSource == null || serializeFuncSource == null) + { + additionalSource = propMetadataInitFuncSource ?? serializeFuncSource; + } + else + { + additionalSource = @$"{propMetadataInitFuncSource}{serializeFuncSource}"; + } + + return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource); } private string GeneratePropMetadataInitFunc( bool declaringTypeIsValueType, - string propInitFuncVarName, - List? properties) + string propInitMethodName, + List? properties) { const string PropVarName = "properties"; const string JsonContextVarName = "jsonContext"; - const string JsonPropertyInfoTypeName = "JsonPropertyInfo"; string propertyArrayInstantiationValue = properties == null - ? $"System.Array.Empty<{JsonPropertyInfoTypeName}>()" - : $"new {JsonPropertyInfoTypeName}[{properties.Count}]"; + ? $"{ArrayTypeRef}.Empty<{JsonPropertyInfoTypeRef}>()" + : $"new {JsonPropertyInfoTypeRef}[{properties.Count}]"; + + string contextTypeRef = _currentContext.ContextTypeRef; StringBuilder sb = new(); sb.Append($@" - private static {JsonPropertyInfoTypeName}[] {propInitFuncVarName}(JsonSerializerContext context) - {{ - JsonContext {JsonContextVarName} = (JsonContext)context; - JsonSerializerOptions options = context.Options; - {JsonPropertyInfoTypeName}[] {PropVarName} = {propertyArrayInstantiationValue}; +private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerContextTypeRef} context) +{{ + {contextTypeRef} {JsonContextVarName} = ({contextTypeRef})context; + {JsonSerializerOptionsTypeRef} options = context.Options; + + {JsonPropertyInfoTypeRef}[] {PropVarName} = {propertyArrayInstantiationValue}; "); if (properties != null) { for (int i = 0; i < properties.Count; i++) { - PropertyMetadata memberMetadata = properties[i]; + PropertyGenerationSpec memberMetadata = properties[i]; - TypeMetadata memberTypeMetadata = memberMetadata.TypeMetadata; + TypeGenerationSpec memberTypeMetadata = memberMetadata.TypeGenerationSpec; string clrPropertyName = memberMetadata.ClrName; - string declaringTypeCompilableName = memberMetadata.DeclaringTypeCompilableName; + string declaringTypeCompilableName = memberMetadata.DeclaringTypeRef; string memberTypeFriendlyName = memberTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen ? "null" - : $"{JsonContextVarName}.{memberTypeMetadata.FriendlyName}"; + : $"{JsonContextVarName}.{memberTypeMetadata.TypeInfoPropertyName}"; string typeTypeInfoNamedArg = $"propertyTypeInfo: {memberTypeFriendlyName}"; @@ -383,15 +578,15 @@ namespace System.Text.Json.SourceGeneration ? @$"jsonPropertyName: ""{memberMetadata.JsonPropertyName}""" : "jsonPropertyName: null"; - string getterNamedArg = memberMetadata.HasGetter + string getterNamedArg = memberMetadata.CanUseGetter ? $"getter: static (obj) => {{ return (({declaringTypeCompilableName})obj).{clrPropertyName}; }}" : "getter: null"; string setterNamedArg; - if (memberMetadata.HasSetter) + if (memberMetadata.CanUseSetter) { string propMutation = declaringTypeIsValueType - ? @$"{{ Unsafe.Unbox<{declaringTypeCompilableName}>(obj).{clrPropertyName} = value; }}" + ? @$"{{ {UnsafeTypeRef}.Unbox<{declaringTypeCompilableName}>(obj).{clrPropertyName} = value; }}" : $@"{{ (({declaringTypeCompilableName})obj).{clrPropertyName} = value; }}"; setterNamedArg = $"setter: static (obj, value) => {propMutation}"; @@ -401,7 +596,7 @@ namespace System.Text.Json.SourceGeneration setterNamedArg = "setter: null"; } - JsonIgnoreCondition? ignoreCondition = memberMetadata.IgnoreCondition; + JsonIgnoreCondition? ignoreCondition = memberMetadata.DefaultIgnoreCondition; string ignoreConditionNamedArg = ignoreCondition.HasValue ? $"ignoreCondition: JsonIgnoreCondition.{ignoreCondition.Value}" : "ignoreCondition: default"; @@ -410,270 +605,442 @@ namespace System.Text.Json.SourceGeneration ? "converter: null" : $"converter: {memberMetadata.ConverterInstantiationLogic}"; - string memberTypeCompilableName = memberTypeMetadata.CompilableName; + string memberTypeCompilableName = memberTypeMetadata.TypeRef; sb.Append($@" - {PropVarName}[{i}] = {JsonMetadataServicesClassName}.CreatePropertyInfo<{memberTypeCompilableName}>( - options, - isProperty: {memberMetadata.IsProperty.ToString().ToLowerInvariant()}, - declaringType: typeof({memberMetadata.DeclaringTypeCompilableName}), - {typeTypeInfoNamedArg}, - {converterNamedArg}, - {getterNamedArg}, - {setterNamedArg}, - {ignoreConditionNamedArg}, - numberHandling: {GetNumberHandlingAsStr(memberMetadata.NumberHandling)}, - propertyName: ""{clrPropertyName}"", - {jsonPropertyNameNamedArg}); - "); + {PropVarName}[{i}] = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>( + options, + isProperty: {memberMetadata.IsProperty.ToString().ToLowerInvariant()}, + declaringType: typeof({memberMetadata.DeclaringTypeRef}), + {typeTypeInfoNamedArg}, + {converterNamedArg}, + {getterNamedArg}, + {setterNamedArg}, + {ignoreConditionNamedArg}, + numberHandling: {GetNumberHandlingAsStr(memberMetadata.NumberHandling)}, + propertyName: ""{clrPropertyName}"", + {jsonPropertyNameNamedArg}); + "); } } sb.Append(@$" - return {PropVarName}; - }}"); + return {PropVarName}; +}}"); return sb.ToString(); } - private string GenerateForType(TypeMetadata typeMetadata, string metadataInitSource, string? additionalSource = null) + private string GenerateFastPathFuncForObject( + string typeInfoTypeRef, + string serializeMethodName, + bool canBeNull, + List? properties) + { + JsonSerializerOptionsAttribute options = _currentContext.SerializerOptions; + + // Add the property names to the context-wide cache; we'll generate the source to initialize them at the end of generation. + string[] runtimePropNames = GetRuntimePropNames(properties, options.NamingPolicy); + _currentContext.RuntimePropertyNames.UnionWith(runtimePropNames); + + StringBuilder sb = new(); + + // Begin method definition + sb.Append($@"{WriterVarName}.WriteStartObject();"); + + if (properties != null) + { + // Provide generation logic for each prop. + for (int i = 0; i < properties.Count; i++) + { + PropertyGenerationSpec propertySpec = properties[i]; + TypeGenerationSpec propertyTypeSpec = propertySpec.TypeGenerationSpec; + + if (propertyTypeSpec.ClassType == ClassType.TypeUnsupportedBySourceGen) + { + continue; + } + + if (propertySpec.IsReadOnly) + { + if (propertySpec.IsProperty) + { + if (options.IgnoreReadOnlyProperties) + { + continue; + } + } + else if (options.IgnoreReadOnlyFields) + { + continue; + } + } + + if (!propertySpec.IsProperty && !propertySpec.HasJsonInclude && !options.IncludeFields) + { + continue; + } + + Type propertyType = propertyTypeSpec.Type; + string propName = $"{runtimePropNames[i]}PropName"; + string propValue = $"{ValueVarName}.{propertySpec.ClrName}"; + string methodArgs = $"{propName}, {propValue}"; + + string? methodToCall = GetWriterMethod(propertyType); + + if (propertyType == _generationSpec.CharType) + { + methodArgs = $"{methodArgs}.ToString()"; + } + + string serializationLogic; + + if (methodToCall != null) + { + serializationLogic = $@" + {methodToCall}({methodArgs});"; + } + else + { + serializationLogic = $@" + {WriterVarName}.WritePropertyName({propName}); + {GetSerializeLogicForNonPrimitiveType(propertyTypeSpec.TypeInfoPropertyName, propValue, propertyTypeSpec.GenerateSerializationLogic)}"; + } + + JsonIgnoreCondition ignoreCondition = propertySpec.DefaultIgnoreCondition ?? options.DefaultIgnoreCondition; + DefaultCheckType defaultCheckType; + bool typeCanBeNull = propertyTypeSpec.CanBeNull; + + switch (ignoreCondition) + { + case JsonIgnoreCondition.WhenWritingNull: + defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.None; + break; + case JsonIgnoreCondition.WhenWritingDefault: + defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default; + break; + default: + defaultCheckType = DefaultCheckType.None; + break; + } + + sb.Append(WrapSerializationLogicInDefaultCheckIfRequired(serializationLogic, propValue, defaultCheckType)); + } + } + + // End method definition + sb.Append($@" + + {WriterVarName}.WriteEndObject();"); + + return GenerateFastPathFuncForType(serializeMethodName, typeInfoTypeRef, sb.ToString(), canBeNull); + } + + private string? GetWriterMethod(Type type) + { + string? method; + if (_generationSpec.IsStringBasedType(type)) + { + method = $"{WriterVarName}.WriteString"; + } + else if (type == _generationSpec.BooleanType) + { + method = $"{WriterVarName}.WriteBoolean"; + } + else if (type == _generationSpec.ByteArrayType) + { + method = $"{WriterVarName}.WriteBase64String"; + } + else if (type == _generationSpec.CharType) + { + method = $"{WriterVarName}.WriteString"; + } + else if (_generationSpec.IsNumberType(type)) + { + method = $"{WriterVarName}.WriteNumber"; + } + else + { + method = null; + } + + return method; + } + + private string GenerateFastPathFuncForType(string serializeMethodName, string typeInfoTypeRef, string serializationLogic, bool canBeNull) + { + return $@" + +private static void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {typeInfoTypeRef} {ValueVarName}) +{{ + {GetEarlyNullCheckSource(canBeNull)} + {serializationLogic} +}}"; + } + + private string GetEarlyNullCheckSource(bool canBeNull) + { + return canBeNull + ? $@"if ({ValueVarName} == null) + {{ + {WriterVarName}.WriteNullValue(); + return; + }} +" + : null; + } + + private string GetSerializeLogicForNonPrimitiveType(string typeInfoPropertyName, string valueToWrite, bool serializationLogicGenerated) + { + string typeInfoRef = $"{_currentContext.ContextTypeRef}.Default.{typeInfoPropertyName}"; + + if (serializationLogicGenerated) + { + return $"{typeInfoPropertyName}{SerializeMethodNameSuffix}({WriterVarName}, {valueToWrite});"; + } + + return $"{JsonSerializerTypeRef}.Serialize({WriterVarName}, {valueToWrite}, {typeInfoRef});"; + } + + private enum DefaultCheckType + { + None, + Null, + Default, + } + + private string WrapSerializationLogicInDefaultCheckIfRequired(string serializationLogic, string propValue, DefaultCheckType defaultCheckType) + { + if (defaultCheckType == DefaultCheckType.None) + { + return serializationLogic; + } + + string defaultLiteral = defaultCheckType == DefaultCheckType.Null ? "null" : "default"; + return $@" + if ({propValue} != {defaultLiteral}) + {{{serializationLogic} + }}"; + } + + private string[] GetRuntimePropNames(List? properties, JsonKnownNamingPolicy namingPolicy) + { + if (properties == null) + { + return Array.Empty(); + } + + int propCount = properties.Count; + string[] runtimePropNames = new string[propCount]; + + // Compute JsonEncodedText values to represent each property name. This gives the best throughput performance + for (int i = 0; i < propCount; i++) + { + PropertyGenerationSpec propertySpec = properties[i]; + + string propName = DetermineRuntimePropName(propertySpec.ClrName, propertySpec.JsonPropertyName, namingPolicy); + Debug.Assert(propName != null); + + runtimePropNames[i] = propName; + } + + return runtimePropNames; + } + + private string DetermineRuntimePropName(string clrPropName, string? jsonPropName, JsonKnownNamingPolicy namingPolicy) { - string typeCompilableName = typeMetadata.CompilableName; - string typeFriendlyName = typeMetadata.FriendlyName; + string runtimePropName; - return @$"{GetUsingStatementsString(typeMetadata)} + if (jsonPropName != null) + { + runtimePropName = jsonPropName; + } + else if (namingPolicy == JsonKnownNamingPolicy.BuiltInCamelCase) + { + runtimePropName = JsonNamingPolicy.CamelCase.ConvertName(clrPropName); + } + else + { + runtimePropName = clrPropName; + } + + return runtimePropName; + } + + private string GenerateForType(TypeGenerationSpec typeMetadata, string metadataInitSource, string? additionalSource = null) + { + string typeCompilableName = typeMetadata.TypeRef; + string typeFriendlyName = typeMetadata.TypeInfoPropertyName; + string typeInfoPropertyTypeRef = $"{JsonTypeInfoTypeRef}<{typeCompilableName}>"; -namespace {_generationNamespace} + return @$"private {typeInfoPropertyTypeRef} _{typeFriendlyName}; +public {typeInfoPropertyTypeRef} {typeFriendlyName} {{ - {JsonContextDeclarationSource} + get {{ - private JsonTypeInfo<{typeCompilableName}> _{typeFriendlyName}; - public JsonTypeInfo<{typeCompilableName}> {typeFriendlyName} + if (_{typeFriendlyName} == null) {{ - get - {{ - if (_{typeFriendlyName} == null) - {{ - {WrapWithCheckForCustomConverterIfRequired(metadataInitSource, typeCompilableName, typeFriendlyName, GetNumberHandlingAsStr(typeMetadata.NumberHandling))} - }} + {WrapWithCheckForCustomConverterIfRequired(metadataInitSource, typeCompilableName, typeFriendlyName, GetNumberHandlingAsStr(typeMetadata.NumberHandling))} + }} - return _{typeFriendlyName}; - }} - }}{additionalSource} + return _{typeFriendlyName}; }} -}} -"; +}}{additionalSource}"; } private string WrapWithCheckForCustomConverterIfRequired(string source, string typeCompilableName, string typeFriendlyName, string numberHandlingNamedArg) { - if (!_honorRuntimeProvidedCustomConverters) + if (_currentContext.SerializerOptions.IgnoreRuntimeCustomConverters) { return source; } - return @$"JsonConverter customConverter; - if ({OptionsInstanceVariableName}.Converters.Count > 0 && (customConverter = {RuntimeCustomConverterFetchingMethodName}(typeof({typeCompilableName}))) != null) - {{ - _{typeFriendlyName} = {JsonMetadataServicesClassName}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, customConverter); - }} - else - {{ - {source.Replace(Environment.NewLine, $"{Environment.NewLine} ")} - }}"; + return @$"{JsonConverterTypeRef} customConverter; + if ({OptionsInstanceVariableName}.Converters.Count > 0 && (customConverter = {RuntimeCustomConverterFetchingMethodName}(typeof({typeCompilableName}))) != null) + {{ + _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, customConverter); + }} + else + {{ + {IndentSource(source, numIndentations: 1)} + }}"; } - private string GetBaseJsonContextImplementation() + private string GetRootJsonContextImplementation() { + string contextTypeRef = _currentContext.ContextTypeRef; + string contextTypeName = _currentContext.ContextType.Name; + StringBuilder sb = new(); - sb.Append(@$"using System.Text.Json; -using System.Text.Json.Serialization; -namespace {_generationNamespace} -{{ - {JsonContextDeclarationSource} - {{ - private static JsonContext s_default; - public static JsonContext Default => s_default ??= new JsonContext(new JsonSerializerOptions()); + sb.Append(@$"{GetLogicForDefaultSerializerOptionsInit()} - public JsonContext() : base(null) - {{ - }} +private static {contextTypeRef} {DefaultContextBackingStaticVarName}; +public static {contextTypeRef} Default => {DefaultContextBackingStaticVarName} ??= new {contextTypeRef}(new {JsonSerializerOptionsTypeRef}({DefaultOptionsStaticVarName})); - public JsonContext(JsonSerializerOptions options) : base(options) - {{ - }} +public {contextTypeName}() : base(null, {DefaultOptionsStaticVarName}) +{{ +}} - {GetFetchLogicForRuntimeSpecifiedCustomConverter()} - }} +public {contextTypeName}({JsonSerializerOptionsTypeRef} options) : base(options, {DefaultOptionsStaticVarName}) +{{ }} -"); + +{GetFetchLogicForRuntimeSpecifiedCustomConverter()}"); return sb.ToString(); } + private string GetLogicForDefaultSerializerOptionsInit() + { + JsonSerializerOptionsAttribute options = _currentContext.SerializerOptions; + + string? namingPolicyInit = options.NamingPolicy == JsonKnownNamingPolicy.BuiltInCamelCase + ? $@" + PropertyNamingPolicy = {JsonNamingPolicyTypeRef}.CamelCase" + : null; + + return $@" +private static {JsonSerializerOptionsTypeRef} {DefaultOptionsStaticVarName} {{ get; }} = new {JsonSerializerOptionsTypeRef}() +{{ + DefaultIgnoreCondition = {JsonIgnoreConditionTypeRef}.{options.DefaultIgnoreCondition}, + IgnoreReadOnlyFields = {options.IgnoreReadOnlyFields.ToString().ToLowerInvariant()}, + IgnoreReadOnlyProperties = {options.IgnoreReadOnlyProperties.ToString().ToLowerInvariant()}, + IncludeFields = {options.IncludeFields.ToString().ToLowerInvariant()}, + WriteIndented = {options.WriteIndented.ToString().ToLowerInvariant()},{namingPolicyInit} +}};"; + } + private string GetFetchLogicForRuntimeSpecifiedCustomConverter() { - if (!_honorRuntimeProvidedCustomConverters) + if (_currentContext.SerializerOptions.IgnoreRuntimeCustomConverters) { return ""; } // TODO (https://github.com/dotnet/runtime/issues/52218): use a dictionary if count > ~15. - return @$"private JsonConverter {RuntimeCustomConverterFetchingMethodName}(System.Type type) - {{ - System.Collections.Generic.IList converters = {OptionsInstanceVariableName}.Converters; + return @$"private {JsonConverterTypeRef} {RuntimeCustomConverterFetchingMethodName}({TypeTypeRef} type) +{{ + {IListTypeRef}<{JsonConverterTypeRef}> converters = {OptionsInstanceVariableName}.Converters; - for (int i = 0; i < converters.Count; i++) - {{ - JsonConverter converter = converters[i]; + for (int i = 0; i < converters.Count; i++) + {{ + {JsonConverterTypeRef} converter = converters[i]; - if (converter.CanConvert(type)) + if (converter.CanConvert(type)) + {{ + if (converter is {JsonConverterFactoryTypeRef} factory) + {{ + converter = factory.CreateConverter(type, {OptionsInstanceVariableName}); + if (converter == null || converter is {JsonConverterFactoryTypeRef}) {{ - if (converter is JsonConverterFactory factory) - {{ - converter = factory.CreateConverter(type, {OptionsInstanceVariableName}); - if (converter == null || converter is JsonConverterFactory) - {{ - throw new System.InvalidOperationException($""The converter '{{factory.GetType()}}' cannot return null or a JsonConverterFactory instance.""); - }} - }} - - return converter; + throw new {InvalidOperationExceptionTypeRef}($""The converter '{{factory.GetType()}}' cannot return null or a JsonConverterFactory instance.""); }} }} - return null; - }}"; + return converter; + }} + }} + + return null; +}}"; } private string GetGetTypeInfoImplementation() { StringBuilder sb = new(); - HashSet usingStatements = new(); - - foreach (TypeMetadata metadata in _rootSerializableTypes.Values) - { - usingStatements.UnionWith(GetUsingStatements(metadata)); - } - - sb.Append(@$"{GetUsingStatementsString(usingStatements)} - -namespace {_generationNamespace} -{{ - {JsonContextDeclarationSource} - {{ - public override JsonTypeInfo GetTypeInfo(System.Type type) - {{"); + sb.Append(@$"public override {JsonTypeInfoTypeRef} GetTypeInfo({TypeTypeRef} type) +{{"); // TODO (https://github.com/dotnet/runtime/issues/52218): Make this Dictionary-lookup-based if root-serializable type count > 64. - foreach (TypeMetadata metadata in _rootSerializableTypes.Values) + foreach (TypeGenerationSpec metadata in _currentContext.RootSerializableTypes) { if (metadata.ClassType != ClassType.TypeUnsupportedBySourceGen) { sb.Append($@" - if (type == typeof({metadata.Type.GetUniqueCompilableTypeName()})) - {{ - return this.{metadata.FriendlyName}; - }} + if (type == typeof({metadata.TypeRef})) + {{ + return this.{metadata.TypeInfoPropertyName}; + }} "); } } sb.Append(@" - return null!; - } - } -} -"); + return null!; +}"); return sb.ToString(); } - private static string GetUsingStatementsString(TypeMetadata typeMetadata) + private string GetPropertyNameInitialization() { - HashSet usingStatements = GetUsingStatements(typeMetadata); - return GetUsingStatementsString(usingStatements); - } + // Ensure metadata for types has already occured. + Debug.Assert(!( + _currentContext.TypesWithMetadataGenerated.Count == 0 + && _currentContext.RuntimePropertyNames.Count > 0)); - private static string GetUsingStatementsString(HashSet usingStatements) - { - string[] usingsArr = usingStatements.ToArray(); - Array.Sort(usingsArr); - return string.Join("\n", usingsArr); - } - - private static HashSet GetUsingStatements(TypeMetadata typeMetadata) - { - HashSet usingStatements = new(); - - // Add library usings. - usingStatements.Add(FormatAsUsingStatement("System.Runtime.CompilerServices")); - usingStatements.Add(FormatAsUsingStatement("System.Text.Json")); - usingStatements.Add(FormatAsUsingStatement("System.Text.Json.Serialization")); - usingStatements.Add(FormatAsUsingStatement("System.Text.Json.Serialization.Metadata")); - - // Add imports to root type. - usingStatements.Add(FormatAsUsingStatement(typeMetadata.Type.Namespace)); - - switch (typeMetadata.ClassType) - { - case ClassType.Nullable: - { - AddUsingStatementsForType(typeMetadata.NullableUnderlyingTypeMetadata!); - } - break; - case ClassType.Enumerable: - { - AddUsingStatementsForType(typeMetadata.CollectionValueTypeMetadata); - } - break; - case ClassType.Dictionary: - { - AddUsingStatementsForType(typeMetadata.CollectionKeyTypeMetadata); - AddUsingStatementsForType(typeMetadata.CollectionValueTypeMetadata); - } - break; - case ClassType.Object: - { - if (typeMetadata.PropertiesMetadata != null) - { - foreach (PropertyMetadata metadata in typeMetadata.PropertiesMetadata) - { - AddUsingStatementsForType(metadata.TypeMetadata); - } - } - } - break; - default: - break; - } + StringBuilder sb = new(); - void AddUsingStatementsForType(TypeMetadata typeMetadata) + foreach (string propName in _currentContext.RuntimePropertyNames) { - usingStatements.Add(FormatAsUsingStatement(typeMetadata.Type.Namespace)); - - if (typeMetadata.CollectionKeyTypeMetadata != null) - { - Debug.Assert(typeMetadata.CollectionValueTypeMetadata != null); - usingStatements.Add(FormatAsUsingStatement(typeMetadata.CollectionKeyTypeMetadata.Type.Namespace)); - } - - if (typeMetadata.CollectionValueTypeMetadata != null) - { - usingStatements.Add(FormatAsUsingStatement(typeMetadata.CollectionValueTypeMetadata.Type.Namespace)); - } + sb.Append($@" +private static {JsonEncodedTextTypeRef} {propName}PropName = {JsonEncodedTextTypeRef}.Encode(""{propName}"");"); } - return usingStatements; + return sb.ToString(); } - private static string FormatAsUsingStatement(string @namespace) => $"using {@namespace};"; + private static string IndentSource(string source, int numIndentations) + { + Debug.Assert(numIndentations >= 1); + return source.Replace(Environment.NewLine, $"{Environment.NewLine}{new string(' ', 4 * numIndentations)}"); // 4 spaces per indentation. + } private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling) => numberHandling.HasValue - ? $"(JsonNumberHandling){(int)numberHandling.Value}" + ? $"({JsonNumberHandlingTypeRef}){(int)numberHandling.Value}" : "default"; private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>"; diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 86a7043..b909e1e 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -11,6 +11,8 @@ using System.Text.Json.Serialization; using System.Text.Json.SourceGeneration.Reflection; using System.Linq; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace System.Text.Json.SourceGeneration { @@ -32,7 +34,7 @@ namespace System.Text.Json.SourceGeneration private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute"; - private readonly Compilation _compilation; + private readonly GeneratorExecutionContext _executionContext; private readonly MetadataLoadContextInternal _metadataLoadContext; @@ -46,6 +48,7 @@ namespace System.Text.Json.SourceGeneration private readonly Type _dateTimeType; private readonly Type _dateTimeOffsetType; private readonly Type _guidType; + private readonly Type _nullableOfTType; private readonly Type _stringType; private readonly Type _uriType; private readonly Type _versionType; @@ -57,12 +60,20 @@ namespace System.Text.Json.SourceGeneration /// /// Type information for member types in input object graphs. /// - private readonly Dictionary _typeMetadataCache = new(); + private readonly Dictionary _typeGenerationSpecCache = new(); - public Parser(Compilation compilation) + private static DiagnosticDescriptor ContextClassesMustBePartial { get; } = new DiagnosticDescriptor( + id: "SYSLIB1032", + title: new LocalizableResourceString(nameof(SR.ContextClassesMustBePartialTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.ContextClassesMustBePartialMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public Parser(in GeneratorExecutionContext executionContext) { - _compilation = compilation; - _metadataLoadContext = new MetadataLoadContextInternal(compilation); + _executionContext = executionContext; + _metadataLoadContext = new MetadataLoadContextInternal(executionContext.Compilation); _ienumerableType = _metadataLoadContext.Resolve(typeof(IEnumerable)); _listOfTType = _metadataLoadContext.Resolve(typeof(List<>)); @@ -74,6 +85,7 @@ namespace System.Text.Json.SourceGeneration _dateTimeType = _metadataLoadContext.Resolve(typeof(DateTime)); _dateTimeOffsetType = _metadataLoadContext.Resolve(typeof(DateTimeOffset)); _guidType = _metadataLoadContext.Resolve(typeof(Guid)); + _nullableOfTType = _metadataLoadContext.Resolve(typeof(Nullable<>)); _stringType = _metadataLoadContext.Resolve(typeof(string)); _uriType = _metadataLoadContext.Resolve(typeof(Uri)); _versionType = _metadataLoadContext.Resolve(typeof(Version)); @@ -81,109 +93,330 @@ namespace System.Text.Json.SourceGeneration PopulateKnownTypes(); } - public Dictionary? GetRootSerializableTypes(List compilationUnits) + public SourceGenerationSpec? GetGenerationSpec(List classDeclarationSyntaxList) { - TypeExtensions.NullableOfTType = _metadataLoadContext.Resolve(typeof(Nullable<>)); + Compilation compilation = _executionContext.Compilation; + INamedTypeSymbol jsonSerializerContextSymbol = compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializerContext"); + INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializableAttribute"); + INamedTypeSymbol jsonSerializerOptionsAttributeSymbol = compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializerOptionsAttribute"); - const string JsonSerializableAttributeName = "System.Text.Json.Serialization.JsonSerializableAttribute"; - INamedTypeSymbol jsonSerializableAttribute = _compilation.GetTypeByMetadataName(JsonSerializableAttributeName); - if (jsonSerializableAttribute == null) + if (jsonSerializerContextSymbol == null || jsonSerializableAttributeSymbol == null || jsonSerializerOptionsAttributeSymbol == null) { return null; } - // Discover serializable types indicated by JsonSerializableAttribute. - Dictionary? rootTypes = null; + List? contextGenSpecList = null; - foreach (CompilationUnitSyntax compilationUnit in compilationUnits) + foreach (ClassDeclarationSyntax classDeclarationSyntax in classDeclarationSyntaxList) { - SemanticModel compilationSemanticModel = _compilation.GetSemanticModel(compilationUnit.SyntaxTree); + CompilationUnitSyntax compilationUnitSyntax = classDeclarationSyntax.FirstAncestorOrSelf(); + SemanticModel compilationSemanticModel = compilation.GetSemanticModel(compilationUnitSyntax.SyntaxTree); + + if (!DerivesFromJsonSerializerContext(classDeclarationSyntax, jsonSerializerContextSymbol, compilationSemanticModel)) + { + continue; + } + + List? rootTypes = null; + JsonSerializerOptionsAttribute? options = null; - foreach (AttributeListSyntax attributeListSyntax in compilationUnit.AttributeLists) + foreach (AttributeListSyntax attributeListSyntax in classDeclarationSyntax.AttributeLists) { AttributeSyntax attributeSyntax = attributeListSyntax.Attributes.First(); IMethodSymbol attributeSymbol = compilationSemanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; - - if (attributeSymbol == null || !jsonSerializableAttribute.Equals(attributeSymbol.ContainingType, SymbolEqualityComparer.Default)) + if (attributeSymbol == null) { - // Not the right attribute. continue; } - // Get JsonSerializableAttribute arguments. - IEnumerable attributeArguments = attributeSyntax.DescendantNodes().Where(node => node is AttributeArgumentSyntax); - - ITypeSymbol? typeSymbol = null; - string? typeInfoPropertyName = null; + INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType; - int i = 0; - foreach (AttributeArgumentSyntax node in attributeArguments) + if (jsonSerializableAttributeSymbol.Equals(attributeContainingTypeSymbol, SymbolEqualityComparer.Default)) { - if (i == 0) + TypeGenerationSpec? metadata = GetRootSerializableType(compilationSemanticModel, attributeSyntax); + if (metadata != null) { - TypeOfExpressionSyntax? typeNode = node.ChildNodes().Single() as TypeOfExpressionSyntax; - if (typeNode != null) - { - ExpressionSyntax typeNameSyntax = (ExpressionSyntax)typeNode.ChildNodes().Single(); - typeSymbol = compilationSemanticModel.GetTypeInfo(typeNameSyntax).ConvertedType; - } + (rootTypes ??= new List()).Add(metadata); } - else if (i == 1) + } + else if (jsonSerializerOptionsAttributeSymbol.Equals(attributeContainingTypeSymbol, SymbolEqualityComparer.Default)) + { + options = GetSerializerOptions(attributeSyntax); + } + } + + if (rootTypes == null) + { + // No types were indicated with [JsonSerializable] + continue; + } + + INamedTypeSymbol contextTypeSymbol = (INamedTypeSymbol)compilationSemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + Debug.Assert(contextTypeSymbol != null); + + if (!TryGetClassDeclarationList(contextTypeSymbol, out List classDeclarationList)) + { + // Class or one of its containing types is not partial so we can't add to it. + _executionContext.ReportDiagnostic(Diagnostic.Create(ContextClassesMustBePartial, Location.None, new string[] { contextTypeSymbol.Name })); + continue; + } + + contextGenSpecList ??= new List(); + contextGenSpecList.Add(new ContextGenerationSpec + { + SerializerOptions = options ?? new JsonSerializerOptionsAttribute(), + ContextType = contextTypeSymbol.AsType(_metadataLoadContext), + RootSerializableTypes = rootTypes, + ContextClassDeclarationList = classDeclarationList + }); + + // Clear the cache of generated metadata between the processing of context classes. + _typeGenerationSpecCache.Clear(); + } + + if (contextGenSpecList == null) + { + return null; + } + + return new SourceGenerationSpec + { + ContextGenerationSpecList = contextGenSpecList, + BooleanType = _booleanType, + ByteArrayType = _byteArrayType, + CharType = _charType, + DateTimeType = _dateTimeType, + DateTimeOffsetType = _dateTimeOffsetType, + GuidType = _guidType, + StringType = _stringType, + NumberTypes = _numberTypes, + }; + } + + // Returns true if a given type derives directly from JsonSerializerContext. + private bool DerivesFromJsonSerializerContext( + ClassDeclarationSyntax classDeclarationSyntax, + INamedTypeSymbol jsonSerializerContextSymbol, + SemanticModel compilationSemanticModel) + { + SeparatedSyntaxList? baseTypeSyntaxList = classDeclarationSyntax.BaseList?.Types; + if (baseTypeSyntaxList == null) + { + return false; + } + + INamedTypeSymbol? match = null; + + foreach (BaseTypeSyntax baseTypeSyntax in baseTypeSyntaxList) + { + INamedTypeSymbol? candidate = compilationSemanticModel.GetSymbolInfo(baseTypeSyntax.Type).Symbol as INamedTypeSymbol; + if (candidate != null && jsonSerializerContextSymbol.Equals(candidate, SymbolEqualityComparer.Default)) + { + match = candidate; + break; + } + } + + return match != null; + } + + private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [NotNullWhenAttribute(true)] out List classDeclarationList) + { + classDeclarationList = new(); + + INamedTypeSymbol currentSymbol = typeSymbol; + + while (currentSymbol != null) + { + ClassDeclarationSyntax? classDeclarationSyntax = currentSymbol.DeclaringSyntaxReferences.First().GetSyntax() as ClassDeclarationSyntax; + + if (classDeclarationSyntax != null) + { + SyntaxTokenList tokenList = classDeclarationSyntax.Modifiers; + int tokenCount = tokenList.Count; + + bool isPartial = false; + + string[] declarationElements = new string[tokenCount + 2]; + + for (int i = 0; i < tokenCount; i++) + { + SyntaxToken token = tokenList[i]; + declarationElements[i] = token.Text; + + if (token.IsKind(SyntaxKind.PartialKeyword)) { - // Obtain the optional TypeInfoPropertyName string property on the attribute, if present. - SyntaxNode? typeInfoPropertyNameNode = node.ChildNodes().ElementAtOrDefault(1); - if (typeInfoPropertyNameNode != null) - { - typeInfoPropertyName = typeInfoPropertyNameNode.GetFirstToken().ValueText; - } + isPartial = true; } + } - i++; + if (!isPartial) + { + classDeclarationList = null; + return false; } - if (typeSymbol == null) + declarationElements[tokenCount] = "class"; + declarationElements[tokenCount + 1] = currentSymbol.Name; + + classDeclarationList.Add(string.Join(" ", declarationElements)); + } + + currentSymbol = currentSymbol.ContainingType; + } + + Debug.Assert(classDeclarationList.Count > 0); + return true; + } + + private TypeGenerationSpec? GetRootSerializableType(SemanticModel compilationSemanticModel, AttributeSyntax attributeSyntax) + { + IEnumerable attributeArguments = attributeSyntax.DescendantNodes().Where(node => node is AttributeArgumentSyntax); + + ITypeSymbol? typeSymbol = null; + string? typeInfoPropertyName = null; + JsonSourceGenerationMode generationMode = default; + + bool seenFirstArg = false; + foreach (AttributeArgumentSyntax node in attributeArguments) + { + if (!seenFirstArg) + { + TypeOfExpressionSyntax? typeNode = node.ChildNodes().Single() as TypeOfExpressionSyntax; + if (typeNode != null) { - continue; + ExpressionSyntax typeNameSyntax = (ExpressionSyntax)typeNode.ChildNodes().Single(); + typeSymbol = compilationSemanticModel.GetTypeInfo(typeNameSyntax).ConvertedType; } + seenFirstArg = true; + } + else + { + IEnumerable childNodes = node.ChildNodes(); - Type type = new TypeWrapper(typeSymbol, _metadataLoadContext); - if (type.Namespace == "") + NameEqualsSyntax? propertyNameNode = childNodes.First() as NameEqualsSyntax; + Debug.Assert(propertyNameNode != null); + + SyntaxNode? propertyValueMode = childNodes.ElementAtOrDefault(1); + if (propertyNameNode.Name.Identifier.ValueText == "TypeInfoPropertyName") + { + typeInfoPropertyName = propertyValueMode.GetFirstToken().ValueText; + } + else { - // typeof() reference where the type's name isn't fully qualified. - // The compilation is not valid and the user needs to fix their code. - // The compiler will notify the user so we don't have to. - return null; + Debug.Assert(propertyNameNode.Name.Identifier.ValueText == "GenerationMode"); + generationMode = (JsonSourceGenerationMode)Enum.Parse(typeof(JsonSourceGenerationMode), propertyValueMode.GetLastToken().ValueText); } + } + } - rootTypes ??= new Dictionary(); - rootTypes[type.FullName] = GetOrAddTypeMetadata(type, typeInfoPropertyName); + if (typeSymbol == null) + { + return null; + } + + Type type = typeSymbol.AsType(_metadataLoadContext); + if (type.Namespace == "") + { + // typeof() reference where the type's name isn't fully qualified. + // The compilation is not valid and the user needs to fix their code. + // The compiler will notify the user so we don't have to. + return null; + } + + TypeGenerationSpec typeGenerationSpec = GetOrAddTypeGenerationSpec(type); + + if (typeInfoPropertyName != null) + { + typeGenerationSpec.TypeInfoPropertyName = typeInfoPropertyName; + } + + ClassType classType = typeGenerationSpec.ClassType; + CollectionType collectionType = typeGenerationSpec.CollectionType; + switch (generationMode) + { + case JsonSourceGenerationMode.MetadataAndSerialization: + break; + case JsonSourceGenerationMode.Metadata: + typeGenerationSpec.GenerateSerializationLogic = false; + break; + case JsonSourceGenerationMode.Serialization: + typeGenerationSpec.GenerateMetadata = false; + break; + default: + throw new InvalidOperationException(); + } + + return typeGenerationSpec; + } + + private static JsonSerializerOptionsAttribute? GetSerializerOptions(AttributeSyntax attributeSyntax) + { + IEnumerable attributeArguments = attributeSyntax.DescendantNodes().Where(node => node is AttributeArgumentSyntax); + + JsonSerializerOptionsAttribute options = new(); + + foreach (AttributeArgumentSyntax node in attributeArguments) + { + IEnumerable childNodes = node.ChildNodes(); + + NameEqualsSyntax? propertyNameNode = childNodes.First() as NameEqualsSyntax; + Debug.Assert(propertyNameNode != null); + + SyntaxNode? propertyValueNode = childNodes.ElementAtOrDefault(1); + string propertyValueStr = propertyValueNode.GetLastToken().ValueText; + + switch (propertyNameNode.Name.Identifier.ValueText) + { + case "DefaultIgnoreCondition": + options.DefaultIgnoreCondition = (JsonIgnoreCondition)Enum.Parse(typeof(JsonIgnoreCondition), propertyValueStr); + break; + case "IgnoreReadOnlyFields": + options.IgnoreReadOnlyFields = bool.Parse(propertyValueStr); + break; + case "IgnoreReadOnlyProperties": + options.IgnoreReadOnlyProperties = bool.Parse(propertyValueStr); + break; + case "IgnoreRuntimeCustomConverters": + options.IgnoreRuntimeCustomConverters = bool.Parse(propertyValueStr); + break; + case "IncludeFields": + options.IncludeFields = bool.Parse(propertyValueStr); + break; + case "NamingPolicy": + options.NamingPolicy = (JsonKnownNamingPolicy)Enum.Parse(typeof(JsonKnownNamingPolicy), propertyValueStr); + break; + case "WriteIndented": + options.WriteIndented = bool.Parse(propertyValueStr); + break; + default: + throw new InvalidOperationException(); } } - return rootTypes; + return options; } - private TypeMetadata GetOrAddTypeMetadata(Type type, string? typeInfoPropertyName = null) + private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type) { - if (_typeMetadataCache.TryGetValue(type, out TypeMetadata? typeMetadata)) + if (_typeGenerationSpecCache.TryGetValue(type, out TypeGenerationSpec? typeMetadata)) { return typeMetadata!; } // Add metadata to cache now to prevent stack overflow when the same type is found somewhere else in the object graph. typeMetadata = new(); - _typeMetadataCache[type] = typeMetadata; + _typeGenerationSpecCache[type] = typeMetadata; ClassType classType; Type? collectionKeyType = null; Type? collectionValueType = null; Type? nullableUnderlyingType = null; - List? propertiesMetadata = null; + List? propertiesMetadata = null; CollectionType collectionType = CollectionType.NotApplicable; ObjectConstructionStrategy constructionStrategy = default; JsonNumberHandling? numberHandling = null; - bool containsOnlyPrimitives = true; bool foundDesignTimeCustomConverter = false; string? converterInstatiationLogic = null; @@ -215,7 +448,7 @@ namespace System.Text.Json.SourceGeneration { classType = ClassType.KnownType; } - else if (type.IsNullableValueType(out nullableUnderlyingType)) + else if (type.IsNullableValueType(_nullableOfTType, out nullableUnderlyingType)) { Debug.Assert(nullableUnderlyingType != null); classType = ClassType.Nullable; @@ -281,7 +514,7 @@ namespace System.Text.Json.SourceGeneration foreach (PropertyInfo propertyInfo in currentType.GetProperties(bindingFlags)) { - PropertyMetadata metadata = GetPropertyMetadata(propertyInfo); + PropertyGenerationSpec metadata = GetPropertyGenerationSpec(propertyInfo); // Ignore indexers. if (propertyInfo.GetIndexParameters().Length > 0) @@ -289,24 +522,17 @@ namespace System.Text.Json.SourceGeneration continue; } - string key = metadata.JsonPropertyName ?? metadata.ClrName; - - if (metadata.HasGetter || metadata.HasSetter) + if (metadata.CanUseGetter || metadata.CanUseSetter) { (propertiesMetadata ??= new()).Add(metadata); } - - if (containsOnlyPrimitives && !IsPrimitive(propertyInfo.PropertyType)) - { - containsOnlyPrimitives = false; - } } foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) { - PropertyMetadata metadata = GetPropertyMetadata(fieldInfo); + PropertyGenerationSpec metadata = GetPropertyGenerationSpec(fieldInfo); - if (metadata.HasGetter || metadata.HasSetter) + if (metadata.CanUseGetter || metadata.CanUseSetter) { (propertiesMetadata ??= new()).Add(metadata); } @@ -315,25 +541,24 @@ namespace System.Text.Json.SourceGeneration } typeMetadata.Initialize( - compilableName: type.GetUniqueCompilableTypeName(), - friendlyName: typeInfoPropertyName ?? type.GetFriendlyTypeName(), + typeRef: type.GetUniqueCompilableTypeName(), + typeInfoPropertyName: type.GetFriendlyTypeName(), type, classType, isValueType: type.IsValueType, numberHandling, propertiesMetadata, collectionType, - collectionKeyTypeMetadata: collectionKeyType != null ? GetOrAddTypeMetadata(collectionKeyType) : null, - collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeMetadata(collectionValueType) : null, + collectionKeyTypeMetadata: collectionKeyType != null ? GetOrAddTypeGenerationSpec(collectionKeyType) : null, + collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeGenerationSpec(collectionValueType) : null, constructionStrategy, - nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeMetadata(nullableUnderlyingType) : null, - converterInstatiationLogic, - containsOnlyPrimitives); + nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeGenerationSpec(nullableUnderlyingType) : null, + converterInstatiationLogic); return typeMetadata; } - private PropertyMetadata GetPropertyMetadata(MemberInfo memberInfo) + private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo) { IList attributeDataList = CustomAttributeData.GetCustomAttributes(memberInfo); @@ -399,8 +624,9 @@ namespace System.Text.Json.SourceGeneration } Type memberCLRType; - bool hasGetter; - bool hasSetter; + bool isReadOnly; + bool canUseGetter; + bool canUseSetter; bool getterIsVirtual = false; bool setterIsVirtual = false; @@ -409,10 +635,10 @@ namespace System.Text.Json.SourceGeneration case PropertyInfo propertyInfo: { MethodInfo setMethod = propertyInfo.SetMethod; - memberCLRType = propertyInfo.PropertyType; - hasGetter = PropertyAccessorCanBeReferenced(propertyInfo.GetMethod, hasJsonInclude); - hasSetter = PropertyAccessorCanBeReferenced(setMethod, hasJsonInclude) && !setMethod.IsInitOnly(); + isReadOnly = setMethod == null; + canUseGetter = PropertyAccessorCanBeReferenced(propertyInfo.GetMethod, hasJsonInclude); + canUseSetter = PropertyAccessorCanBeReferenced(setMethod, hasJsonInclude) && !setMethod.IsInitOnly(); getterIsVirtual = propertyInfo.GetMethod?.IsVirtual == true; setterIsVirtual = propertyInfo.SetMethod?.IsVirtual == true; } @@ -420,30 +646,31 @@ namespace System.Text.Json.SourceGeneration case FieldInfo fieldInfo: { Debug.Assert(fieldInfo.IsPublic); - memberCLRType = fieldInfo.FieldType; - hasGetter = true; - hasSetter = !fieldInfo.IsInitOnly; + isReadOnly = fieldInfo.IsInitOnly; + canUseGetter = true; + canUseSetter = !isReadOnly; } break; default: throw new InvalidOperationException(); } - return new PropertyMetadata + return new PropertyGenerationSpec { ClrName = memberInfo.Name, IsProperty = memberInfo.MemberType == MemberTypes.Property, JsonPropertyName = jsonPropertyName, - HasGetter = hasGetter, - HasSetter = hasSetter, + IsReadOnly = isReadOnly, + CanUseGetter = canUseGetter, + CanUseSetter = canUseSetter, GetterIsVirtual = getterIsVirtual, SetterIsVirtual = setterIsVirtual, - IgnoreCondition = ignoreCondition, + DefaultIgnoreCondition = ignoreCondition, NumberHandling = numberHandling, HasJsonInclude = hasJsonInclude, - TypeMetadata = GetOrAddTypeMetadata(memberCLRType), - DeclaringTypeCompilableName = memberInfo.DeclaringType.GetUniqueCompilableTypeName(), + TypeGenerationSpec = GetOrAddTypeGenerationSpec(memberCLRType), + DeclaringTypeRef = $"global::{memberInfo.DeclaringType.GetUniqueCompilableTypeName()}", ConverterInstantiationLogic = converterInstantiationLogic }; } @@ -458,7 +685,8 @@ namespace System.Text.Json.SourceGeneration return null; } - Type converterType = new TypeWrapper((ITypeSymbol)attributeData.ConstructorArguments[0].Value, _metadataLoadContext); + ITypeSymbol converterTypeSymbol = (ITypeSymbol)attributeData.ConstructorArguments[0].Value; + Type converterType = converterTypeSymbol.AsType(_metadataLoadContext); if (converterType == null || converterType.GetConstructor(Type.EmptyTypes) == null || converterType.IsNestedPrivate) { @@ -509,9 +737,6 @@ namespace System.Text.Json.SourceGeneration _knownTypes.Add(_metadataLoadContext.Resolve(typeof(Version))); } - - private bool IsPrimitive(Type type) - => _knownTypes.Contains(type) && type != _uriType && type != _versionType; } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs index eb23225..d49dfd0 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs @@ -19,12 +19,6 @@ namespace System.Text.Json.SourceGeneration public sealed partial class JsonSourceGenerator : ISourceGenerator { /// - /// Helper for unit tests. - /// - public Dictionary? GetSerializableTypes() => _rootTypes?.ToDictionary(p => p.Key, p => p.Value.Type); - private Dictionary? _rootTypes; - - /// /// Registers a syntax resolver to receive compilation units. /// /// @@ -39,32 +33,42 @@ namespace System.Text.Json.SourceGeneration /// public void Execute(GeneratorExecutionContext executionContext) { + //if (!Diagnostics.Debugger.IsAttached) { Diagnostics.Debugger.Launch(); }; SyntaxReceiver receiver = (SyntaxReceiver)executionContext.SyntaxReceiver; - List compilationUnits = receiver.CompilationUnits; - if (compilationUnits == null) + List? contextClasses = receiver.ClassDeclarationSyntaxList; + if (contextClasses == null) { return; } - Parser parser = new(executionContext.Compilation); - _rootTypes = parser.GetRootSerializableTypes(receiver.CompilationUnits); - - if (_rootTypes != null) + Parser parser = new(executionContext); + SourceGenerationSpec? spec = parser.GetGenerationSpec(receiver.ClassDeclarationSyntaxList); + if (spec != null) { - Emitter emitter = new(executionContext, _rootTypes); + _rootTypes = spec.ContextGenerationSpecList[0].RootSerializableTypes; + + Emitter emitter = new(executionContext, spec); emitter.Emit(); } } - internal sealed class SyntaxReceiver : ISyntaxReceiver + private const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration"; + + /// + /// Helper for unit tests. + /// + public Dictionary? GetSerializableTypes() => _rootTypes?.ToDictionary(p => p.Type.FullName, p => p.Type); + private List? _rootTypes; + + private sealed class SyntaxReceiver : ISyntaxReceiver { - public List? CompilationUnits { get; private set; } + public List? ClassDeclarationSyntaxList { get; private set; } public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { - if (syntaxNode is CompilationUnitSyntax compilationUnit) + if (syntaxNode is ClassDeclarationSyntax cds) { - (CompilationUnits ??= new List()).Add(compilationUnit); + (ClassDeclarationSyntaxList ??= new List()).Add(cds); } } } diff --git a/src/libraries/System.Text.Json/gen/PropertyMetadata.cs b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs similarity index 80% rename from src/libraries/System.Text.Json/gen/PropertyMetadata.cs rename to src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs index 68a5a40..25ae736 100644 --- a/src/libraries/System.Text.Json/gen/PropertyMetadata.cs +++ b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; namespace System.Text.Json.SourceGeneration { [DebuggerDisplay("Name={Name}, Type={TypeMetadata}")] - internal class PropertyMetadata + internal sealed class PropertyGenerationSpec { /// /// The CLR name of the property. @@ -25,16 +25,21 @@ namespace System.Text.Json.SourceGeneration public string? JsonPropertyName { get; init; } /// + /// Whether the property has a set method. + /// + public bool IsReadOnly { get; init; } + + /// /// Whether the property has a public or internal (only usable when JsonIncludeAttribute is specified) /// getter that can be referenced in generated source code. /// - public bool HasGetter { get; init; } + public bool CanUseGetter { get; init; } /// /// Whether the property has a public or internal (only usable when JsonIncludeAttribute is specified) /// setter that can be referenced in generated source code. /// - public bool HasSetter { get; init; } + public bool CanUseSetter { get; init; } public bool GetterIsVirtual { get; init; } @@ -43,7 +48,7 @@ namespace System.Text.Json.SourceGeneration /// /// The for the property. /// - public JsonIgnoreCondition? IgnoreCondition { get; init; } + public JsonIgnoreCondition? DefaultIgnoreCondition { get; init; } /// /// The for the property. @@ -56,14 +61,14 @@ namespace System.Text.Json.SourceGeneration public bool HasJsonInclude { get; init; } /// - /// Metadata for the property's type. + /// Generation specification for the property's type. /// - public TypeMetadata TypeMetadata { get; init; } + public TypeGenerationSpec TypeGenerationSpec { get; init; } /// /// Compilable name of the property's declaring type. /// - public string DeclaringTypeCompilableName { get; init; } + public string DeclaringTypeRef { get; init; } /// /// Source code to instantiate design-time specified custom converter. diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs index 4bfb71a..8ff487a 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs @@ -38,16 +38,14 @@ namespace System.Text.Json.SourceGeneration.Reflection return compilableName.Replace(".", "").Replace("<", "").Replace(">", "").Replace(",", "").Replace("[]", "Array"); } - public static Type NullableOfTType { get; set; } - - public static bool IsNullableValueType(this Type type, out Type? underlyingType) + public static bool IsNullableValueType(this Type type, Type nullableOfTType, out Type? underlyingType) { - Debug.Assert(NullableOfTType != null); + Debug.Assert(nullableOfTType != null); // TODO: log bug because Nullable.GetUnderlyingType doesn't work due to // https://github.com/dotnet/runtimelab/blob/7472c863db6ec5ddab7f411ddb134a6e9f3c105f/src/libraries/System.Private.CoreLib/src/System/Nullable.cs#L124 // i.e. type.GetGenericTypeDefinition() will never equal typeof(Nullable<>), as expected in that code segment. - if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableOfTType) + if (type.IsGenericType && type.GetGenericTypeDefinition() == nullableOfTType) { underlyingType = type.GetGenericArguments()[0]; return true; @@ -57,6 +55,22 @@ namespace System.Text.Json.SourceGeneration.Reflection return false; } + public static bool IsNullableValueType(this Type type, out Type? underlyingType) + { + if (type.IsGenericType && type.Name.StartsWith("Nullable`1")) + { + underlyingType = type.GetGenericArguments()[0]; + return true; + } + + underlyingType = null; + return false; + } + + public static bool IsObjectType(this Type type) => type.FullName == "System.Object"; + + public static bool IsStringType(this Type type) => type.FullName == "System.String"; + public static Type? GetCompatibleBaseClass(this Type type, string baseTypeFullName) { Type? baseTypeToCheck = type; diff --git a/src/libraries/System.Text.Json/gen/Resources/Strings.resx b/src/libraries/System.Text.Json/gen/Resources/Strings.resx index 4560355..d6d4a15 100644 --- a/src/libraries/System.Text.Json/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/gen/Resources/Strings.resx @@ -129,4 +129,10 @@ Did not generate serialization metadata for type. + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + \ No newline at end of file diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf index 85182d5..d27ae15 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf index 1b94670..d2e73c0 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf index 55915ce..176e213 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf index cb63078..288e44e 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf index 2ac4e6f..434a86f 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf index 4661920..1ed4855 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf index 322cc79f..e503e6b 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf index 408e27c..728d28a 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf index e131d25..b02aae1 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf index f02ca1c..ebfd3c9 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf index 2a00a92..0e59048 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf index 2822193..27a56b7 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf index c5c1cc1..9ba73dc 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -2,6 +2,16 @@ + + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation. + + + + Derived 'JsonSerializerContext' types and all containing types must be partial. + Derived 'JsonSerializerContext' types and all containing types must be partial. + + There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. diff --git a/src/libraries/System.Text.Json/gen/SourceGenerationSpec.cs b/src/libraries/System.Text.Json/gen/SourceGenerationSpec.cs new file mode 100644 index 0000000..0019dc5 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/SourceGenerationSpec.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Text.Json.SourceGeneration +{ + internal sealed class SourceGenerationSpec + { + public List ContextGenerationSpecList { get; init; } + + public Type BooleanType { get; init; } + public Type ByteArrayType { get; init; } + public Type CharType { get; init; } + public Type DateTimeType { private get; init; } + public Type DateTimeOffsetType { private get; init; } + public Type GuidType { private get; init; } + public Type StringType { private get; init; } + + public HashSet NumberTypes { private get; init; } + + public bool IsStringBasedType(Type type) + => type == StringType || type == DateTimeType || type == DateTimeOffsetType || type == GuidType; + + public bool IsNumberType(Type type) => NumberTypes.Contains(type); + } +} diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj index 74dd28a..5ed7281 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj @@ -22,15 +22,21 @@ + + + + + + - + @@ -44,6 +50,8 @@ - + + + diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs new file mode 100644 index 0000000..20155f0 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json.Serialization; +using System.Text.Json.SourceGeneration.Reflection; + +namespace System.Text.Json.SourceGeneration +{ + [DebuggerDisplay("Type={Type}, ClassType={ClassType}")] + internal class TypeGenerationSpec + { + /// + /// Fully qualified assembly name, prefixed with "global::", e.g. global::System.Numerics.BigInteger. + /// + public string TypeRef { get; private set; } + + /// + /// The name of the public JsonTypeInfo property for this type on the generated context class. + /// For example, if the context class is named MyJsonContext, and the value of this property is JsonMessage; + /// then users will call MyJsonContext.JsonMessage to access generated metadata for the type. + /// + public string TypeInfoPropertyName { get; set; } + + public bool GenerateMetadata { get; set; } = true; + + private bool? _generateSerializationLogic; + public bool GenerateSerializationLogic + { + get => _generateSerializationLogic ??= FastPathIsSupported(); + set => _generateSerializationLogic = value; + } + + public Type Type { get; private set; } + + public ClassType ClassType { get; private set; } + + public bool IsValueType { get; private set; } + + public bool CanBeNull { get; private set; } + + public JsonNumberHandling? NumberHandling { get; private set; } + + public List? PropertiesMetadata { get; private set; } + + public CollectionType CollectionType { get; private set; } + + public TypeGenerationSpec? CollectionKeyTypeMetadata { get; private set; } + + public TypeGenerationSpec? CollectionValueTypeMetadata { get; private set; } + + public ObjectConstructionStrategy ConstructionStrategy { get; private set; } + + public TypeGenerationSpec? NullableUnderlyingTypeMetadata { get; private set; } + + public string? ConverterInstantiationLogic { get; private set; } + + public void Initialize( + string typeRef, + string typeInfoPropertyName, + Type type, + ClassType classType, + bool isValueType, + JsonNumberHandling? numberHandling, + List? propertiesMetadata, + CollectionType collectionType, + TypeGenerationSpec? collectionKeyTypeMetadata, + TypeGenerationSpec? collectionValueTypeMetadata, + ObjectConstructionStrategy constructionStrategy, + TypeGenerationSpec? nullableUnderlyingTypeMetadata, + string? converterInstantiationLogic) + { + TypeRef = $"global::{typeRef}"; + TypeInfoPropertyName = typeInfoPropertyName; + Type = type; + ClassType = classType; + IsValueType = isValueType; + CanBeNull = !isValueType || nullableUnderlyingTypeMetadata != null; + NumberHandling = numberHandling; + PropertiesMetadata = propertiesMetadata; + CollectionType = collectionType; + CollectionKeyTypeMetadata = collectionKeyTypeMetadata; + CollectionValueTypeMetadata = collectionValueTypeMetadata; + ConstructionStrategy = constructionStrategy; + NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata; + ConverterInstantiationLogic = converterInstantiationLogic; + } + + public bool FastPathIsSupported() + { + if (ClassType == ClassType.Object) + { + return true; + } + + if (CollectionType == CollectionType.Array || CollectionType == CollectionType.List) + { + return !CollectionValueTypeMetadata!.Type.IsObjectType(); + } + + if (CollectionType == CollectionType.Dictionary) + { + return CollectionKeyTypeMetadata!.Type.IsStringType() && !CollectionValueTypeMetadata!.Type.IsObjectType(); + } + + return false; + } + } +} diff --git a/src/libraries/System.Text.Json/gen/TypeMetadata.cs b/src/libraries/System.Text.Json/gen/TypeMetadata.cs deleted file mode 100644 index 7f9ea63..0000000 --- a/src/libraries/System.Text.Json/gen/TypeMetadata.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; -using System.Text.Json.Serialization; - -namespace System.Text.Json.SourceGeneration -{ - [DebuggerDisplay("Type={Type}, ClassType={ClassType}")] - internal class TypeMetadata - { - private bool _hasBeenInitialized; - - public string CompilableName { get; private set; } - - public string FriendlyName { get; private set; } - - public Type Type { get; private set; } - - public ClassType ClassType { get; private set; } - - public bool IsValueType { get; private set; } - - public JsonNumberHandling? NumberHandling { get; private set; } - - public List? PropertiesMetadata { get; private set; } - - public CollectionType CollectionType { get; private set; } - - public TypeMetadata? CollectionKeyTypeMetadata { get; private set; } - - public TypeMetadata? CollectionValueTypeMetadata { get; private set; } - - public ObjectConstructionStrategy ConstructionStrategy { get; private set; } - - public TypeMetadata? NullableUnderlyingTypeMetadata { get; private set; } - - public string? ConverterInstantiationLogic { get; private set; } - - public bool ContainsOnlyPrimitives { get; private set; } - - public void Initialize( - string compilableName, - string friendlyName, - Type type, - ClassType classType, - bool isValueType, - JsonNumberHandling? numberHandling, - List? propertiesMetadata, - CollectionType collectionType, - TypeMetadata? collectionKeyTypeMetadata, - TypeMetadata? collectionValueTypeMetadata, - ObjectConstructionStrategy constructionStrategy, - TypeMetadata? nullableUnderlyingTypeMetadata, - string? converterInstantiationLogic, - bool containsOnlyPrimitives) - { - if (_hasBeenInitialized) - { - throw new InvalidOperationException("Type metadata has already been initialized."); - } - - _hasBeenInitialized = true; - - CompilableName = compilableName; - FriendlyName = friendlyName; - Type = type; - ClassType = classType; - IsValueType = isValueType; - NumberHandling = numberHandling; - PropertiesMetadata = propertiesMetadata; - CollectionType = collectionType; - CollectionKeyTypeMetadata = collectionKeyTypeMetadata; - CollectionValueTypeMetadata = collectionValueTypeMetadata; - ConstructionStrategy = constructionStrategy; - NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata; - ConverterInstantiationLogic = converterInstantiationLogic; - ContainsOnlyPrimitives = containsOnlyPrimitives; - } - } -} diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 6664460..efc83f0 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -777,6 +777,11 @@ namespace System.Text.Json.Serialization { public JsonIncludeAttribute() { } } + public enum JsonKnownNamingPolicy + { + Unspecified = 0, + BuiltInCamelCase = 1, + } [System.FlagsAttribute] public enum JsonNumberHandling { @@ -797,18 +802,38 @@ namespace System.Text.Json.Serialization public JsonPropertyNameAttribute(string name) { } public string Name { get { throw null; } } } - [System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple=true)] + [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true)] public sealed partial class JsonSerializableAttribute : System.Text.Json.Serialization.JsonAttribute { public JsonSerializableAttribute(System.Type type) { } public string? TypeInfoPropertyName { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonSourceGenerationMode GenerationMode { get { throw null; } set { } } } public abstract partial class JsonSerializerContext { - protected JsonSerializerContext(System.Text.Json.JsonSerializerOptions? options) { } + protected JsonSerializerContext(System.Text.Json.JsonSerializerOptions? instanceOptions, System.Text.Json.JsonSerializerOptions? defaultOptions) { } public System.Text.Json.JsonSerializerOptions Options { get { throw null; } } public abstract System.Text.Json.Serialization.Metadata.JsonTypeInfo? GetTypeInfo(System.Type type); } + [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=false)] + public partial class JsonSerializerOptionsAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonSerializerOptionsAttribute() { } + public System.Text.Json.Serialization.JsonIgnoreCondition DefaultIgnoreCondition { get { throw null; } set { } } + public bool IgnoreReadOnlyFields { get { throw null; } set { } } + public bool IgnoreReadOnlyProperties { get { throw null; } set { } } + public bool IgnoreRuntimeCustomConverters { get { throw null; } set { } } + public bool IncludeFields { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonKnownNamingPolicy NamingPolicy { get { throw null; } set { } } + public bool WriteIndented { get { throw null; } set { } } + } + [System.FlagsAttribute] + public enum JsonSourceGenerationMode + { + MetadataAndSerialization = 0, + Metadata = 1, + Serialization = 2, + } public sealed partial class JsonStringEnumConverter : System.Text.Json.Serialization.JsonConverterFactory { public JsonStringEnumConverter() { } @@ -870,15 +895,14 @@ namespace System.Text.Json.Serialization.Metadata public static System.Text.Json.Serialization.JsonConverter UInt64Converter { get { throw null; } } public static System.Text.Json.Serialization.JsonConverter UriConverter { get { throw null; } } public static System.Text.Json.Serialization.JsonConverter VersionConverter { get { throw null; } } - public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateArrayInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonTypeInfo elementInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling) { throw null; } - public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateDictionaryInfo(System.Text.Json.JsonSerializerOptions options, System.Func createObjectFunc, System.Text.Json.Serialization.Metadata.JsonTypeInfo keyInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo valueInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling) where TCollection : System.Collections.Generic.Dictionary where TKey : notnull { throw null; } - public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateListInfo(System.Text.Json.JsonSerializerOptions options, System.Func? createObjectFunc, System.Text.Json.Serialization.Metadata.JsonTypeInfo elementInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling) where TCollection : System.Collections.Generic.List { throw null; } - public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateObjectInfo() where T : notnull { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateArrayInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonTypeInfo elementInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling, System.Action? serializeFunc) { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateDictionaryInfo(System.Text.Json.JsonSerializerOptions options, System.Func createObjectFunc, System.Text.Json.Serialization.Metadata.JsonTypeInfo keyInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo valueInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling, System.Action? serializeFunc) where TCollection : System.Collections.Generic.Dictionary where TKey : notnull { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateListInfo(System.Text.Json.JsonSerializerOptions options, System.Func? createObjectFunc, System.Text.Json.Serialization.Metadata.JsonTypeInfo elementInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling, System.Action? serializeFunc) where TCollection : System.Collections.Generic.List { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateObjectInfo(System.Text.Json.JsonSerializerOptions options, System.Func? createObjectFunc, System.Func? propInitFunc, System.Text.Json.Serialization.JsonNumberHandling numberHandling, System.Action? serializeFunc) where T : notnull { throw null; } public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo(System.Text.Json.JsonSerializerOptions options, bool isProperty, System.Type declaringType, System.Text.Json.Serialization.Metadata.JsonTypeInfo propertyTypeInfo, System.Text.Json.Serialization.JsonConverter? converter, System.Func? getter, System.Action? setter, System.Text.Json.Serialization.JsonIgnoreCondition ignoreCondition, System.Text.Json.Serialization.JsonNumberHandling numberHandling, string propertyName, string? jsonPropertyName) { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateValueInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; } public static System.Text.Json.Serialization.JsonConverter GetEnumConverter(System.Text.Json.JsonSerializerOptions options) where T : struct { throw null; } public static System.Text.Json.Serialization.JsonConverter GetNullableConverter(System.Text.Json.Serialization.Metadata.JsonTypeInfo underlyingTypeInfo) where T : struct { throw null; } - public static void InitializeObjectInfo(System.Text.Json.Serialization.Metadata.JsonTypeInfo info, System.Text.Json.JsonSerializerOptions options, System.Func? createObjectFunc, System.Func propInitFunc, System.Text.Json.Serialization.JsonNumberHandling numberHandling) where T : notnull { } } public abstract partial class JsonPropertyInfo { @@ -891,5 +915,6 @@ namespace System.Text.Json.Serialization.Metadata public abstract partial class JsonTypeInfo : System.Text.Json.Serialization.Metadata.JsonTypeInfo { internal JsonTypeInfo() { } + public System.Action? Serialize { get { throw null; } } } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index c68c367..9f73cb0 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -596,4 +596,13 @@ A custom converter for JsonObject is not allowed on an extension property. + + 'propInitFunc' and 'serializeFunc' cannot both be 'null'. + + + 'JsonSerializerContext' '{0}' did not provide property metadata for type '{1}'. + + + To specify a serialization implementation for type '{0}'', context '{0}' must specify default options. + \ No newline at end of file diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 90c3695..ed42782 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -1,4 +1,4 @@ - + true $(NetCoreAppCurrent);netstandard2.0;netcoreapp3.0;net461 @@ -22,8 +22,14 @@ + + + + + + @@ -78,7 +84,6 @@ - @@ -87,7 +92,7 @@ - + @@ -168,15 +173,12 @@ - - - diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonSerializableAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonSerializableAttribute.cs index 9b58024..b223fad 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonSerializableAttribute.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonSerializableAttribute.cs @@ -9,10 +9,16 @@ namespace System.Text.Json.Serialization /// Instructs the System.Text.Json source generator to generate source code to help optimize performance /// when serializing and deserializing instances of the specified type and types in its object graph. /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class JsonSerializableAttribute : JsonAttribute { /// + /// Initializes a new instance of with the specified type. + /// + /// The type to generate source code for. + public JsonSerializableAttribute(Type type) { } + + /// /// The name of the property for the generated for /// the type on the generated, derived type. /// @@ -22,9 +28,8 @@ namespace System.Text.Json.Serialization public string? TypeInfoPropertyName { get; set; } /// - /// Initializes a new instance of with the specified type. + /// Determines what the source generator should generate for the type. /// - /// The type to generate source code for. - public JsonSerializableAttribute(Type type) { } + public JsonSourceGenerationMode GenerationMode { get; set; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs new file mode 100644 index 0000000..d28134e --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Provides a mechanism to invoke "fast-path" serialization logic via + /// . This type holds an optional + /// reference to an actual for the type + /// , to provide a fallback when the fast path cannot be used. + /// + /// The type to converter + internal sealed class JsonMetadataServicesConverter : JsonResumableConverter + { + private readonly Func> _converterCreator; + + private readonly ConverterStrategy _converterStrategy; + + private readonly Type? _keyType; + private readonly Type? _elementType; + + private JsonConverter? _converter; + + // A backing converter for when fast-path logic cannot be used. + private JsonConverter Converter + { + get + { + _converter ??= _converterCreator(); + Debug.Assert(_converter != null); + Debug.Assert(_converter.ConverterStrategy == _converterStrategy); + Debug.Assert(_converter.KeyType == _keyType); + Debug.Assert(_converter.ElementType == _elementType); + return _converter; + } + } + + internal override ConverterStrategy ConverterStrategy => _converterStrategy; + + internal override Type? KeyType => _keyType; + + internal override Type? ElementType => _elementType; + + public JsonMetadataServicesConverter(Func> converterCreator, ConverterStrategy converterStrategy, Type? keyType, Type? elementType) + { + _converterCreator = converterCreator ?? throw new ArgumentNullException(nameof(converterCreator)); + _converterStrategy = converterStrategy; + _keyType = keyType; + _elementType = elementType; + } + + internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value) + { + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + + if (_converterStrategy == ConverterStrategy.Object && jsonTypeInfo.PropertyCache == null) + { + jsonTypeInfo.InitializeDeserializePropCache(); + } + + return Converter.OnTryRead(ref reader, typeToConvert, options, ref state, out value); + } + + internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) + { + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + + Debug.Assert(options == jsonTypeInfo.Options); + + if (!state.SupportContinuation && + jsonTypeInfo is JsonTypeInfo info && + info.Serialize != null && + info.Options._context?.CanUseSerializationLogic == true) + { + info.Serialize(writer, value); + return true; + } + + if (_converterStrategy == ConverterStrategy.Object && jsonTypeInfo.PropertyCacheArray == null) + { + jsonTypeInfo.InitializeSerializePropCache(); + } + + return Converter.OnTryWrite(writer, value, options, ref state); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 63f48da..234c2c2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -15,6 +15,8 @@ namespace System.Text.Json.Serialization.Converters { internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value) { + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + object obj; if (state.UseFastPath) @@ -26,12 +28,12 @@ namespace System.Text.Json.Serialization.Converters ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); } - if (state.Current.JsonTypeInfo.CreateObject == null) + if (jsonTypeInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(state.Current.JsonTypeInfo.Type, ref reader, ref state); + ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo.Type, ref reader, ref state); } - obj = state.Current.JsonTypeInfo.CreateObject!()!; + obj = jsonTypeInfo.CreateObject!()!; // Process all properties. while (true) @@ -98,12 +100,12 @@ namespace System.Text.Json.Serialization.Converters if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) { - if (state.Current.JsonTypeInfo.CreateObject == null) + if (jsonTypeInfo.CreateObject == null) { - ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(state.Current.JsonTypeInfo.Type, ref reader, ref state); + ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo.Type, ref reader, ref state); } - obj = state.Current.JsonTypeInfo.CreateObject!()!; + obj = jsonTypeInfo.CreateObject!()!; state.Current.ReturnValue = obj; state.Current.ObjectState = StackFrameObjectState.CreatedObject; @@ -216,7 +218,7 @@ namespace System.Text.Json.Serialization.Converters // Check if we are trying to build the sorted cache. if (state.Current.PropertyRefCache != null) { - state.Current.JsonTypeInfo.UpdateSortedPropertyCache(ref state.Current); + jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current); } value = (T)obj; @@ -224,12 +226,14 @@ namespace System.Text.Json.Serialization.Converters return true; } - internal override bool OnTryWrite( + internal sealed override bool OnTryWrite( Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) { + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + // Minimize boxing for structs by only boxing once here object objectValue = value!; @@ -244,10 +248,10 @@ namespace System.Text.Json.Serialization.Converters } } - JsonPropertyInfo? dataExtensionProperty = state.Current.JsonTypeInfo.DataExtensionProperty; + JsonPropertyInfo? dataExtensionProperty = jsonTypeInfo.DataExtensionProperty; int propertyCount = 0; - JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonTypeInfo.PropertyCacheArray; + JsonPropertyInfo[]? propertyCacheArray = jsonTypeInfo.PropertyCacheArray; if (propertyCacheArray != null) { propertyCount = propertyCacheArray.Length; @@ -302,10 +306,10 @@ namespace System.Text.Json.Serialization.Converters state.Current.ProcessedStartToken = true; } - JsonPropertyInfo? dataExtensionProperty = state.Current.JsonTypeInfo.DataExtensionProperty; + JsonPropertyInfo? dataExtensionProperty = jsonTypeInfo.DataExtensionProperty; int propertyCount = 0; - JsonPropertyInfo[]? propertyCacheArray = state.Current.JsonTypeInfo.PropertyCacheArray; + JsonPropertyInfo[]? propertyCacheArray = jsonTypeInfo.PropertyCacheArray; if (propertyCacheArray != null) { propertyCount = propertyCacheArray.Length; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectSourceGenConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectSourceGenConverter.cs deleted file mode 100644 index 70f377d..0000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectSourceGenConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization.Metadata; - -namespace System.Text.Json.Serialization.Converters -{ - /// - /// Implementation of JsonObjectConverter{T} for source-generated converters. - /// - internal sealed class ObjectSourceGenConverter : ObjectDefaultConverter where T : notnull - { - internal override bool OnTryRead( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options, - ref ReadStack state, - [MaybeNullWhen(false)] out T value) - { - JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; - if (jsonTypeInfo.PropertyCache == null) - { - jsonTypeInfo.InitializeDeserializePropCache(); - } - - return base.OnTryRead(ref reader, typeToConvert, options, ref state, out value); - } - - internal override bool OnTryWrite( - Utf8JsonWriter writer, - T value, - JsonSerializerOptions options, - ref WriteStack state) - { - JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; - if (jsonTypeInfo.PropertyCacheArray == null) - { - jsonTypeInfo.InitializeSerializePropCache(); - } - - return base.OnTryWrite(writer, value, options, ref state); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDefaultNamingPolicy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDefaultNamingPolicy.cs deleted file mode 100644 index 300096f..0000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDefaultNamingPolicy.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Text.Json -{ - internal sealed class JsonDefaultNamingPolicy : JsonNamingPolicy - { - public override string ConvertName(string name) => name; - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs index e1d7eb1..ccf639b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; - namespace System.Text.Json.Serialization { /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs index f212494..50c5eac 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs @@ -41,15 +41,24 @@ namespace System.Text.Json private static void WriteUsingMetadata(Utf8JsonWriter writer, in TValue value, JsonTypeInfo jsonTypeInfo) { - WriteStack state = default; - state.Initialize(jsonTypeInfo, supportContinuation: false); + if (jsonTypeInfo is JsonTypeInfo typedInfo && + typedInfo.Serialize != null && + typedInfo.Options._context?.CanUseSerializationLogic == true) + { + typedInfo.Serialize(writer, value); + } + else + { + WriteStack state = default; + state.Initialize(jsonTypeInfo, supportContinuation: false); - JsonConverter converter = jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; - Debug.Assert(converter != null); + JsonConverter converter = jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + Debug.Assert(converter != null); - Debug.Assert(jsonTypeInfo.Options != null); + Debug.Assert(jsonTypeInfo.Options != null); - WriteCore(converter, writer, value, jsonTypeInfo.Options, ref state); + WriteCore(converter, writer, value, jsonTypeInfo.Options, ref state); + } } private static Type GetRuntimeType(in TValue value) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs index 12dd595..78843cf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs @@ -12,9 +12,48 @@ namespace System.Text.Json.Serialization [EditorBrowsable(EditorBrowsableState.Never)] public abstract partial class JsonSerializerContext { + private bool? _canUseSerializationLogic; + private JsonSerializerOptions? _defaultOptions; + internal JsonSerializerOptions? _options; /// + /// Indicates whether pre-generated serialization logic for types in the context + /// is compatible with the run-time specified . + /// + internal bool CanUseSerializationLogic + { + get + { + if (!_canUseSerializationLogic.HasValue) + { + if (_defaultOptions == null) + { + _canUseSerializationLogic = false; + } + else + { + _canUseSerializationLogic = + // Guard against unsupported features + Options.Converters.Count == 0 && + Options.Encoder == null && + Options.NumberHandling == JsonNumberHandling.Strict && + Options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.None && + // Ensure options values are consistent with expected defaults. + Options.DefaultIgnoreCondition == _defaultOptions.DefaultIgnoreCondition && + Options.IgnoreReadOnlyFields == _defaultOptions.IgnoreReadOnlyFields && + Options.IgnoreReadOnlyProperties == _defaultOptions.IgnoreReadOnlyProperties && + Options.IncludeFields == _defaultOptions.IncludeFields && + Options.PropertyNamingPolicy == _defaultOptions.PropertyNamingPolicy && + Options.WriteIndented == _defaultOptions.WriteIndented; + } + } + + return _canUseSerializationLogic.Value; + } + } + + /// /// Gets the run-time specified options of the context. If no options were passed /// when instanciating the context, then a new instance is bound and returned. /// @@ -38,23 +77,26 @@ namespace System.Text.Json.Serialization /// /// Creates an instance of and binds it with the indicated . /// - /// The run-time provided options for the context instance. + /// The run-time provided options for the context instance. + /// The default run-time options for the context. It's values are defined at design-time via . /// - /// If no options are passed, then no options are set until the context is bound using , + /// If no instance options are passed, then no options are set until the context is bound using , /// or until is called, where a new options instance is created and bound. /// - protected JsonSerializerContext(JsonSerializerOptions? options) + protected JsonSerializerContext(JsonSerializerOptions? instanceOptions, JsonSerializerOptions? defaultOptions) { - if (options != null) + if (instanceOptions != null) { - if (options._context != null) + if (instanceOptions._context != null) { ThrowHelper.ThrowInvalidOperationException_JsonSerializerOptionsAlreadyBoundToContext(); } - _options = options; - options._context = this; + _options = instanceOptions; + instanceOptions._context = this; } + + _defaultOptions = defaultOptions; } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs index e4782e9..12f3717 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs @@ -15,17 +15,21 @@ namespace System.Text.Json.Serialization.Metadata /// The to use. /// A instance representing the element type. /// The option to apply to number collection elements. + /// An optimized serialization implementation assuming pre-determined defaults. /// public static JsonTypeInfo CreateArrayInfo( JsonSerializerOptions options, JsonTypeInfo elementInfo, - JsonNumberHandling numberHandling) + JsonNumberHandling numberHandling, + Action? serializeFunc) => new JsonTypeInfoInternal( options, createObjectFunc: null, - new ArrayConverter(), + () => new ArrayConverter(), elementInfo, - numberHandling); + numberHandling, + serializeFunc, + typeof(TElement)); /// /// Creates metadata for types assignable to . @@ -36,19 +40,23 @@ namespace System.Text.Json.Serialization.Metadata /// A to create an instance of the list when deserializing. /// A instance representing the element type. /// The option to apply to number collection elements. + /// An optimized serialization implementation assuming pre-determined defaults. /// public static JsonTypeInfo CreateListInfo( JsonSerializerOptions options, Func? createObjectFunc, JsonTypeInfo elementInfo, - JsonNumberHandling numberHandling) + JsonNumberHandling numberHandling, + Action? serializeFunc) where TCollection : List => new JsonTypeInfoInternal( options, createObjectFunc, - new ListOfTConverter(), + () => new ListOfTConverter(), elementInfo, - numberHandling); + numberHandling, + serializeFunc, + typeof(TElement)); /// /// Creates metadata for types assignable to . @@ -61,21 +69,26 @@ namespace System.Text.Json.Serialization.Metadata /// A instance representing the key type. /// A instance representing the value type. /// The option to apply to number collection elements. + /// An optimized serialization implementation assuming pre-determined defaults. /// public static JsonTypeInfo CreateDictionaryInfo( JsonSerializerOptions options, Func createObjectFunc, JsonTypeInfo keyInfo, JsonTypeInfo valueInfo, - JsonNumberHandling numberHandling) + JsonNumberHandling numberHandling, + Action? serializeFunc) where TCollection : Dictionary where TKey : notnull => new JsonTypeInfoInternal( options, createObjectFunc, - new DictionaryOfTKeyTValueConverter(), + () => new DictionaryOfTKeyTValueConverter(), keyInfo, valueInfo, - numberHandling); + numberHandling, + serializeFunc, + typeof(TKey), + typeof(TValue)); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs index 681a201e..0be5273 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs @@ -147,7 +147,7 @@ namespace System.Text.Json.Serialization.Metadata /// Creates a instance that converts values. /// /// The generic definition for the enum type. - /// + /// The to use for serialization and deserialization. /// public static JsonConverter GetEnumConverter(JsonSerializerOptions options) where T : struct, Enum => new EnumConverter(EnumConverterOptions.AllowNumbers, options ?? throw new ArgumentNullException(nameof(options))); @@ -155,8 +155,8 @@ namespace System.Text.Json.Serialization.Metadata /// /// Creates a instance that converts values. /// - /// - /// + /// The generic definition for the underlying nullable type. + /// Serialization metadata for the underlying nullable type. /// public static JsonConverter GetNullableConverter(JsonTypeInfo underlyingTypeInfo) where T : struct { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs index 27a2fe1..14fd403 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs @@ -16,6 +16,17 @@ namespace System.Text.Json.Serialization.Metadata /// Creates metadata for a property or field. /// /// The type that the converter for the property returns or accepts when converting JSON data. + /// The to initialize the metadata with. + /// Whether the CLR member is a property or field. + /// The declaring type of the property or field. + /// The info for the property or field's type. + /// A for the property or field, specified by . + /// Provides a mechanism to get the property or field's value. + /// Provides a mechanism to set the property or field's value. + /// Specifies a condition for the property to be ignored. + /// If the property or field is a number, specifies how it should processed when serializing and deserializing. + /// The CLR name of the property or field. + /// The name to be used when processing the property or field, specified by . /// A instance intialized with the provided metadata. public static JsonPropertyInfo CreatePropertyInfo( JsonSerializerOptions options, @@ -79,53 +90,21 @@ namespace System.Text.Json.Serialization.Metadata /// /// Creates metadata for a complex class or struct. /// + /// The to initialize the metadata with. + /// Provides a mechanism to create an instance of the class or struct when deserializing. + /// Provides a mechanism to initialize metadata for properties and fields of the class or struct. + /// Provides a serialization implementation for instances of the class or struct which assumes options specified by . + /// Specifies how number properties and fields should be processed when serializing and deserializing. /// The type of the class or struct. + /// Thrown when and are both null. /// A instance representing the class or struct. - public static JsonTypeInfo CreateObjectInfo() where T : notnull => new JsonTypeInfoInternal(); - - /// - /// Initializes metadata for a class or struct. - /// - /// The type of the class or struct - /// - /// - /// - /// - /// - /// Thrown when , , or is null. - /// Thrown when , does not represent a complex class or struct type. - public static void InitializeObjectInfo( - JsonTypeInfo info, + public static JsonTypeInfo CreateObjectInfo( JsonSerializerOptions options, Func? createObjectFunc, - Func propInitFunc, - JsonNumberHandling numberHandling) - where T : notnull - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - if (info.PropertyInfoForTypeInfo != null) - { - // ConverterStrategy.Object is the only info type we won't have set PropertyInfoForTypeInfo for at this point. - throw new ArgumentException(SR.InitializeTypeInfoAsObjectInvalid, nameof(info)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (propInitFunc == null) - { - throw new ArgumentNullException(nameof(propInitFunc)); - } - - ((JsonTypeInfoInternal)info).InitializeAsObject(options, createObjectFunc, propInitFunc, numberHandling); - Debug.Assert(info.PropertyInfoForTypeInfo!.ConverterStrategy == ConverterStrategy.Object); - } + Func? propInitFunc, + JsonNumberHandling numberHandling, + Action? serializeFunc) where T : notnull + => new JsonTypeInfoInternal(options, createObjectFunc, propInitFunc, numberHandling, serializeFunc); /// /// Creates metadata for a primitive or a type with a custom converter. @@ -134,7 +113,7 @@ namespace System.Text.Json.Serialization.Metadata /// A instance representing the type. public static JsonTypeInfo CreateValueInfo(JsonSerializerOptions options, JsonConverter converter) { - JsonTypeInfo info = new JsonTypeInfoInternal(options); + JsonTypeInfo info = new JsonTypeInfoInternal(options, ConverterStrategy.Value); info.PropertyInfoForTypeInfo = CreateJsonPropertyInfoForClassInfo(typeof(T), info, converter, options); return info; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index 39dffda..f6cbe62 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -568,14 +568,24 @@ namespace System.Text.Json.Serialization.Metadata internal void InitializeSerializePropCache() { - Debug.Assert(PropInitFunc != null); - Debug.Assert(Options._context != null); + JsonSerializerContext? context = Options._context; - PropertyCacheArray = PropInitFunc(Options._context); + Debug.Assert(context != null); + Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object); + + if (PropInitFunc == null) + { + ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(context, Type); + return; + } + + PropertyCacheArray = PropInitFunc(context); } internal void InitializeDeserializePropCache() { + Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object); + if (PropertyCacheArray == null) { InitializeSerializePropCache(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index e61da17..2de65bf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -149,15 +149,8 @@ namespace System.Text.Json.Serialization.Metadata internal JsonTypeInfo(Type type, JsonSerializerOptions options, ConverterStrategy converterStrategy) { - // Options setting for object class types is deferred till initialization. - if (converterStrategy != ConverterStrategy.Object && options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - Options = options!; Type = type; - + Options = options ?? throw new ArgumentNullException(nameof(options)); // Setting this option is deferred to the initialization methods of the various metadada info types. PropertyInfoForTypeInfo = null!; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs index 38fe706..b016ef9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs @@ -12,18 +12,40 @@ namespace System.Text.Json.Serialization.Metadata internal sealed class JsonTypeInfoInternal : JsonTypeInfo { /// - /// Creates serialization metadata for a . + /// Creates serialization metadata given JsonSerializerOptions and a ConverterStrategy. /// - public JsonTypeInfoInternal() : base(typeof(T), null!, ConverterStrategy.Object) + public JsonTypeInfoInternal(JsonSerializerOptions options, ConverterStrategy converterStrategy) + : base(typeof(T), options, converterStrategy) { } /// - /// Creates serialization metadata for a . + /// Creates serialization metadata for an object. /// - public JsonTypeInfoInternal(JsonSerializerOptions options) - : base (typeof(T), options, ConverterStrategy.Value) + public JsonTypeInfoInternal( + JsonSerializerOptions options, + Func? createObjectFunc, + Func? propInitFunc, + JsonNumberHandling numberHandling, + Action? serializeFunc + ) : base(typeof(T), options, ConverterStrategy.Object) { + if (propInitFunc == null && serializeFunc == null) + { + ThrowHelper.ThrowInvalidOperationException_PropInitAndSerializeFuncsNull(); + } + +#pragma warning disable CS8714 + // The type cannot be used as type parameter in the generic type or method. + // Nullability of type argument doesn't match 'notnull' constraint. + JsonConverter converter = new JsonMetadataServicesConverter(() => new ObjectDefaultConverter(), ConverterStrategy.Object, keyType: null, elementType: null); +#pragma warning restore CS8714 + + PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, Options); + NumberHandling = numberHandling; + PropInitFunc = propInitFunc; + Serialize = serializeFunc; + SetCreateObjectFunc(createObjectFunc); } /// @@ -32,14 +54,19 @@ namespace System.Text.Json.Serialization.Metadata public JsonTypeInfoInternal( JsonSerializerOptions options, Func? createObjectFunc, - JsonConverter converter, - JsonTypeInfo elementInfo, - JsonNumberHandling numberHandling) : base(typeof(T), options, ConverterStrategy.Enumerable) + Func> converterCreator, + JsonTypeInfo? elementInfo, + JsonNumberHandling numberHandling, + Action? serializeFunc, + Type elementType) : base(typeof(T), options, ConverterStrategy.Enumerable) { + JsonConverter converter = new JsonMetadataServicesConverter(converterCreator, ConverterStrategy.Enumerable, keyType: null, elementType); + ElementType = converter.ElementType; ElementTypeInfo = elementInfo ?? throw new ArgumentNullException(nameof(elementInfo)); NumberHandling = numberHandling; PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, options); + Serialize = serializeFunc; SetCreateObjectFunc(createObjectFunc); } @@ -49,40 +76,24 @@ namespace System.Text.Json.Serialization.Metadata public JsonTypeInfoInternal( JsonSerializerOptions options, Func? createObjectFunc, - JsonConverter converter, - JsonTypeInfo keyInfo, - JsonTypeInfo valueInfo, - JsonNumberHandling numberHandling) : base(typeof(T), options, ConverterStrategy.Dictionary) + Func> converterCreator, + JsonTypeInfo? keyInfo, + JsonTypeInfo? valueInfo, + JsonNumberHandling numberHandling, + Action? serializeFunc, + Type keyType, + Type elementType) : base(typeof(T), options, ConverterStrategy.Dictionary) { + JsonConverter converter = new JsonMetadataServicesConverter(converterCreator, ConverterStrategy.Dictionary, keyType, elementType); + KeyType = converter.KeyType; - KeyTypeInfo = keyInfo ?? throw new ArgumentNullException(nameof(keyInfo)); ; + ElementType = converter.ElementType; + KeyTypeInfo = keyInfo ?? throw new ArgumentNullException(nameof(keyInfo)); ElementType = converter.ElementType; ElementTypeInfo = valueInfo ?? throw new ArgumentNullException(nameof(valueInfo)); NumberHandling = numberHandling; PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, options); - SetCreateObjectFunc(createObjectFunc); - } - - /// - /// Initializes serialization metadata for a . - /// - public void InitializeAsObject( - JsonSerializerOptions options, - Func? createObjectFunc, - Func propInitFunc, - JsonNumberHandling numberHandling) - { - Options = options; - -#pragma warning disable CS8714 - // The type cannot be used as type parameter in the generic type or method. - // Nullability of type argument doesn't match 'notnull' constraint. - JsonConverter converter = new ObjectSourceGenConverter(); -#pragma warning restore CS8714 - - PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, options); - NumberHandling = numberHandling; - PropInitFunc = propInitFunc; + Serialize = serializeFunc; SetCreateObjectFunc(createObjectFunc); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs index 6d3b7f0..208ac31 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs @@ -21,5 +21,11 @@ namespace System.Text.Json.Serialization.Metadata { Debug.Assert(false, "This constructor should not be called."); } + + /// + /// A method that serializes an instance of using + /// values specified at design time. + /// + public Action? Serialize { get; private protected set; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index ab7cf89..7beb061 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -699,5 +699,22 @@ namespace System.Text.Json { throw new InvalidOperationException(SR.Format(SR.NoMetadataForType, type)); } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_PropInitAndSerializeFuncsNull() + { + throw new InvalidOperationException(SR.Format(SR.PropInitAndSerializeFuncsNull)); + } + + public static void ThrowInvalidOperationException_NoMetadataForTypeProperties(JsonSerializerContext context, Type type) + { + throw new InvalidOperationException(SR.Format(SR.NoMetadataForTypeProperties, context.GetType(), type)); + } + + public static void ThrowInvalidOperationException_NoDefaultOptionsForContext(JsonSerializerContext context, Type type) + { + throw new InvalidOperationException(SR.Format(SR.NoDefaultOptionsForContext, context.GetType(), type)); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs new file mode 100644 index 0000000..8d53fc3 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using Xunit; + +namespace System.Text.Json +{ + internal static partial class JsonTestHelper + { + public static void AssertJsonEqual(string expected, string actual) + { + using JsonDocument expectedDom = JsonDocument.Parse(expected); + using JsonDocument actualDom = JsonDocument.Parse(actual); + AssertJsonEqual(expectedDom.RootElement, actualDom.RootElement); + } + + private static void AssertJsonEqual(JsonElement expected, JsonElement actual) + { + JsonValueKind valueKind = expected.ValueKind; + Assert.Equal(valueKind, actual.ValueKind); + + switch (valueKind) + { + case JsonValueKind.Object: + var propertyNames = new HashSet(); + + foreach (JsonProperty property in expected.EnumerateObject()) + { + propertyNames.Add(property.Name); + } + + foreach (JsonProperty property in actual.EnumerateObject()) + { + propertyNames.Add(property.Name); + } + + foreach (string name in propertyNames) + { + AssertJsonEqual(expected.GetProperty(name), actual.GetProperty(name)); + } + break; + case JsonValueKind.Array: + JsonElement.ArrayEnumerator expectedEnumerator = actual.EnumerateArray(); + JsonElement.ArrayEnumerator actualEnumerator = expected.EnumerateArray(); + + while (expectedEnumerator.MoveNext()) + { + Assert.True(actualEnumerator.MoveNext()); + AssertJsonEqual(expectedEnumerator.Current, actualEnumerator.Current); + } + + Assert.False(actualEnumerator.MoveNext()); + break; + case JsonValueKind.String: + Assert.Equal(expected.GetString(), actual.GetString()); + break; + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + Assert.Equal(expected.GetRawText(), actual.GetRawText()); + break; + default: + Debug.Fail($"Unexpected JsonValueKind: JsonValueKind.{valueKind}."); + break; + } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs new file mode 100644 index 0000000..e7cde22 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public interface ITestContext + { + public JsonTypeInfo Location { get; } + public JsonTypeInfo RepeatedLocation { get; } + public JsonTypeInfo ActiveOrUpcomingEvent { get; } + public JsonTypeInfo CampaignSummaryViewModel { get; } + public JsonTypeInfo IndexViewModel { get; } + public JsonTypeInfo WeatherForecastWithPOCOs { get; } + public JsonTypeInfo EmptyPoco { get; } + public JsonTypeInfo HighLowTemps { get; } + public JsonTypeInfo MyType { get; } + public JsonTypeInfo MyType2 { get; } + public JsonTypeInfo MyIntermediateType { get; } + public JsonTypeInfo HighLowTempsImmutable { get; } + public JsonTypeInfo MyNestedClass { get; } + public JsonTypeInfo MyNestedNestedClass { get; } + public JsonTypeInfo ObjectArray { get; } + public JsonTypeInfo String { get; } + public JsonTypeInfo ClassWithEnumAndNullable { get; } + } + + internal partial class JsonContext : JsonSerializerContext + { + private static JsonSerializerOptions s_defaultOptions { get; } = new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + private static JsonContext s_defaultContext; + public static JsonContext Default => s_defaultContext ??= new JsonContext(new JsonSerializerOptions(s_defaultOptions)); + + public JsonContext() : base(null, s_defaultOptions) + { + } + + public JsonContext(JsonSerializerOptions options) : base(options, s_defaultOptions) + { + } + + public override JsonTypeInfo GetTypeInfo(global::System.Type type) + { + if (type == typeof(JsonMessage)) + { + return JsonMessage; + } + + return null!; + } + + private JsonTypeInfo _JsonMessage; + public JsonTypeInfo JsonMessage + { + get + { + if (_JsonMessage == null) + { + JsonTypeInfo objectInfo = JsonMetadataServices.CreateObjectInfo( + Options, + createObjectFunc: static () => new JsonMessage(), + propInitFunc: null, + default, + serializeFunc: JsonMessageSerialize); + + _JsonMessage = objectInfo; + } + + return _JsonMessage; + } + } + + private static void JsonMessageSerialize(Utf8JsonWriter writer, JsonMessage value) => throw new NotImplementedException(); + } + + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary))] + internal partial class DictionaryTypeContext : JsonSerializerContext { } + + [JsonSerializable(typeof(JsonMessage))] + public partial class PublicContext : JsonSerializerContext { } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs new file mode 100644 index 0000000..77882f6 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public static partial class JsonSerializerContextTests + { + [Fact] + public static void VariousNestingAndVisibilityLevelsAreSupported() + { + Assert.NotNull(PublicContext.Default); + Assert.NotNull(NestedContext.Default); + Assert.NotNull(NestedPublicContext.Default); + Assert.NotNull(NestedPublicContext.NestedProtectedInternalClass.Default); + } + + [JsonSerializable(typeof(JsonMessage))] + internal partial class NestedContext : JsonSerializerContext { } + + [JsonSerializable(typeof(JsonMessage))] + public partial class NestedPublicContext : JsonSerializerContext + { + [JsonSerializable(typeof(JsonMessage))] + protected internal partial class NestedProtectedInternalClass : JsonSerializerContext { } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonTestHelper.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonTestHelper.cs new file mode 100644 index 0000000..66b2020 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonTestHelper.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json +{ + internal static partial class JsonTestHelper + { + internal static void AssertThrows_PropMetadataInit(Action action, Type type) + { + var ex = Assert.Throws(action); + string exAsStr = ex.ToString(); + Assert.Contains(type.ToString(), exAsStr); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs new file mode 100644 index 0000000..a5ffbfb --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + [JsonSerializable(typeof(Location))] + [JsonSerializable(typeof(RepeatedTypes.Location), TypeInfoPropertyName = "RepeatedLocation")] + [JsonSerializable(typeof(ActiveOrUpcomingEvent))] + [JsonSerializable(typeof(CampaignSummaryViewModel))] + [JsonSerializable(typeof(IndexViewModel))] + [JsonSerializable(typeof(WeatherForecastWithPOCOs))] + [JsonSerializable(typeof(EmptyPoco))] + // Ensure no errors when type of member in previously specified object graph is passed as input type to generator. + [JsonSerializable(typeof(HighLowTemps))] + [JsonSerializable(typeof(MyType))] + [JsonSerializable(typeof(MyType2))] + [JsonSerializable(typeof(MyIntermediateType))] + [JsonSerializable(typeof(HighLowTempsImmutable))] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass))] + [JsonSerializable(typeof(object[]))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] + internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext + { + } + + public sealed class MetadataAndSerializationContextTests : RealWorldContextTests + { + public MetadataAndSerializationContextTests() : base(MetadataAndSerializationContext.Default, (options) => new MetadataAndSerializationContext(options)) { } + + [Fact] + public override void EnsureFastPathGeneratedAsExpected() + { + Assert.NotNull(MetadataAndSerializationContext.Default.Location.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.RepeatedLocation.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.ActiveOrUpcomingEvent.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.CampaignSummaryViewModel.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.IndexViewModel.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.WeatherForecastWithPOCOs.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.EmptyPoco.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTemps.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.MyType.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.MyType2.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.MyIntermediateType.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTempsImmutable.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedClass.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedNestedClass.Serialize); + Assert.Null(MetadataAndSerializationContext.Default.ObjectArray.Serialize); + Assert.Null(MetadataAndSerializationContext.Default.String.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithEnumAndNullable.Serialize); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs new file mode 100644 index 0000000..5d4b469 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + [JsonSerializable(typeof(Location), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(RepeatedTypes.Location), TypeInfoPropertyName = "RepeatedLocation", GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ActiveOrUpcomingEvent), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(CampaignSummaryViewModel), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(IndexViewModel), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(WeatherForecastWithPOCOs), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(EmptyPoco), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata)] + internal partial class MetadataContext : JsonSerializerContext, ITestContext + { + } + + public sealed class MetadataContextTests : RealWorldContextTests + { + public MetadataContextTests() : base(MetadataContext.Default, (options) => new MetadataContext(options)) { } + + [Fact] + public override void EnsureFastPathGeneratedAsExpected() + { + Assert.Null(MetadataContext.Default.Location.Serialize); + Assert.Null(MetadataContext.Default.RepeatedLocation.Serialize); + Assert.Null(MetadataContext.Default.ActiveOrUpcomingEvent.Serialize); + Assert.Null(MetadataContext.Default.CampaignSummaryViewModel.Serialize); + Assert.Null(MetadataContext.Default.IndexViewModel.Serialize); + Assert.Null(MetadataContext.Default.WeatherForecastWithPOCOs.Serialize); + Assert.Null(MetadataContext.Default.EmptyPoco.Serialize); + Assert.Null(MetadataContext.Default.HighLowTemps.Serialize); + Assert.Null(MetadataContext.Default.MyType.Serialize); + Assert.Null(MetadataContext.Default.MyType2.Serialize); + Assert.Null(MetadataContext.Default.MyIntermediateType.Serialize); + Assert.Null(MetadataContext.Default.HighLowTempsImmutable.Serialize); + Assert.Null(MetadataContext.Default.MyNestedClass.Serialize); + Assert.Null(MetadataContext.Default.MyNestedNestedClass.Serialize); + Assert.Null(MetadataContext.Default.ObjectArray.Serialize); + Assert.Null(MetadataContext.Default.String.Serialize); + Assert.Null(MetadataContext.Default.ClassWithEnumAndNullable.Serialize); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs new file mode 100644 index 0000000..a92759a --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + [JsonSerializable(typeof(Location), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(RepeatedTypes.Location), TypeInfoPropertyName = "RepeatedLocation", GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ActiveOrUpcomingEvent), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(CampaignSummaryViewModel), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(IndexViewModel), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(WeatherForecastWithPOCOs), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(EmptyPoco), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.MetadataAndSerialization)] + [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.MetadataAndSerialization)] + [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.MetadataAndSerialization)] + [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.MetadataAndSerialization)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.MetadataAndSerialization)] + internal partial class MixedModeContext : JsonSerializerContext, ITestContext + { + } + + public sealed class MixedModeContextTests : RealWorldContextTests + { + public MixedModeContextTests() : base(MixedModeContext.Default, (options) => new MixedModeContext(options)) { } + + [Fact] + public override void EnsureFastPathGeneratedAsExpected() + { + Assert.Null(MixedModeContext.Default.Location.Serialize); + Assert.NotNull(MixedModeContext.Default.RepeatedLocation.Serialize); + Assert.NotNull(MixedModeContext.Default.CampaignSummaryViewModel.Serialize); + Assert.Null(MixedModeContext.Default.IndexViewModel.Serialize); + Assert.Null(MixedModeContext.Default.WeatherForecastWithPOCOs.Serialize); + Assert.NotNull(MixedModeContext.Default.EmptyPoco.Serialize); + Assert.NotNull(MixedModeContext.Default.HighLowTemps.Serialize); + Assert.NotNull(MixedModeContext.Default.MyType.Serialize); + Assert.NotNull(MixedModeContext.Default.MyType2.Serialize); + Assert.NotNull(MixedModeContext.Default.MyIntermediateType.Serialize); + Assert.Null(MixedModeContext.Default.HighLowTempsImmutable.Serialize); + Assert.NotNull(MixedModeContext.Default.MyNestedClass.Serialize); + Assert.NotNull(MixedModeContext.Default.MyNestedNestedClass.Serialize); + Assert.Null(MixedModeContext.Default.ObjectArray.Serialize); + Assert.Null(MixedModeContext.Default.String.Serialize); + Assert.NotNull(MixedModeContext.Default.ClassWithEnumAndNullable.Serialize); + } + + [Fact] + public override void RoundTripIndexViewModel() + { + IndexViewModel expected = CreateIndexViewModel(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.IndexViewModel); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.IndexViewModel), typeof(CampaignSummaryViewModel)); + + IndexViewModel obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).IndexViewModel); + VerifyIndexViewModel(expected, obj); + } + + [Fact] + public override void RoundTripCampaignSummaryViewModel() + { + CampaignSummaryViewModel expected = CreateCampaignSummaryViewModel(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.CampaignSummaryViewModel); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.CampaignSummaryViewModel), typeof(CampaignSummaryViewModel)); + + CampaignSummaryViewModel obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).CampaignSummaryViewModel); + VerifyCampaignSummaryViewModel(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.CampaignSummaryViewModel); + } + + [Fact] + public override void RoundTripCollectionsDictionary() + { + WeatherForecastWithPOCOs expected = CreateWeatherForecastWithPOCOs(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.WeatherForecastWithPOCOs); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.WeatherForecastWithPOCOs), typeof(HighLowTemps)); + + WeatherForecastWithPOCOs obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).WeatherForecastWithPOCOs); + VerifyWeatherForecastWithPOCOs(expected, obj); + } + + [Fact] + public override void RoundTripEmptyPoco() + { + EmptyPoco expected = CreateEmptyPoco(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.EmptyPoco); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.EmptyPoco), typeof(EmptyPoco)); + + EmptyPoco obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).EmptyPoco); + VerifyEmptyPoco(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.EmptyPoco); + } + + [Fact] + public override void RoundTripTypeNameClash() + { + RepeatedTypes.Location expected = CreateRepeatedLocation(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.RepeatedLocation); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.RepeatedLocation), typeof(RepeatedTypes.Location)); + + RepeatedTypes.Location obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).RepeatedLocation); + VerifyRepeatedLocation(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.RepeatedLocation); + } + + [Fact] + public override void HandlesNestedTypes() + { + string json = @"{""MyInt"":5}"; + MyNestedClass obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).MyNestedClass); + Assert.Equal(5, obj.MyInt); + Assert.Equal(json, JsonSerializer.Serialize(obj, DefaultContext.MyNestedClass)); + + MyNestedClass.MyNestedNestedClass obj2 = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).MyNestedNestedClass); + Assert.Equal(5, obj2.MyInt); + Assert.Equal(json, JsonSerializer.Serialize(obj2, DefaultContext.MyNestedNestedClass)); + } + + [Fact] + public override void SerializeObjectArray() + { + IndexViewModel index = CreateIndexViewModel(); + CampaignSummaryViewModel campaignSummary = CreateCampaignSummaryViewModel(); + + string json = JsonSerializer.Serialize(new object[] { index, campaignSummary }, DefaultContext.ObjectArray); + object[] arr = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).ObjectArray); + + JsonElement indexAsJsonElement = (JsonElement)arr[0]; + JsonElement campaignSummeryAsJsonElement = (JsonElement)arr[1]; + VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), ((ITestContext)MetadataContext.Default).IndexViewModel)); + VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), ((ITestContext)MetadataContext.Default).CampaignSummaryViewModel)); + } + + [Fact] + public override void SerializeObjectArray_WithCustomOptions() + { + IndexViewModel index = CreateIndexViewModel(); + CampaignSummaryViewModel campaignSummary = CreateCampaignSummaryViewModel(); + + ITestContext context = SerializationContextWithCamelCase.Default; + Assert.Same(JsonNamingPolicy.CamelCase, ((JsonSerializerContext)context).Options.PropertyNamingPolicy); + + string json = JsonSerializer.Serialize(new object[] { index, campaignSummary }, context.ObjectArray); + object[] arr = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).ObjectArray); + + JsonElement indexAsJsonElement = (JsonElement)arr[0]; + JsonElement campaignSummeryAsJsonElement = (JsonElement)arr[1]; + + ITestContext metadataContext = new MetadataContext(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), metadataContext.IndexViewModel)); + VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), metadataContext.CampaignSummaryViewModel)); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs similarity index 74% rename from src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs rename to src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index cc14bdc..83902a9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -2,101 +2,105 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using System.Text.Json.SourceGeneration.Tests; -using System.Text.Json.SourceGeneration.Tests.JsonSourceGeneration; using Xunit; -[assembly: JsonSerializable(typeof(JsonSerializerSourceGeneratorTests.MyNestedClass))] -[assembly: JsonSerializable(typeof(JsonSerializerSourceGeneratorTests.MyNestedClass.MyNestedNestedClass))] -[assembly: JsonSerializable(typeof(object[]))] -[assembly: JsonSerializable(typeof(string))] -[assembly: JsonSerializable(typeof(JsonSerializerSourceGeneratorTests.ClassWithEnumAndNullable))] - namespace System.Text.Json.SourceGeneration.Tests { - public static class JsonSerializerSourceGeneratorTests + public abstract class RealWorldContextTests { + protected ITestContext DefaultContext { get; } + private Func _contextCreator; + + public RealWorldContextTests(ITestContext defaultContext, Func contextCreator) + { + DefaultContext = defaultContext; + _contextCreator = contextCreator; + } + + public abstract void EnsureFastPathGeneratedAsExpected(); + [Fact] - public static void RoundTripLocation() + public virtual void RoundTripLocation() { Location expected = CreateLocation(); - string json = JsonSerializer.Serialize(expected, JsonContext.Default.Location); - Location obj = JsonSerializer.Deserialize(json, JsonContext.Default.Location); + string json = JsonSerializer.Serialize(expected, DefaultContext.Location); + Location obj = JsonSerializer.Deserialize(json, DefaultContext.Location); VerifyLocation(expected, obj); } [Fact] - public static void RoundTripIndexViewModel() + public virtual void RoundTripIndexViewModel() { IndexViewModel expected = CreateIndexViewModel(); - string json = JsonSerializer.Serialize(expected, JsonContext.Default.IndexViewModel); - IndexViewModel obj = JsonSerializer.Deserialize(json, JsonContext.Default.IndexViewModel); + string json = JsonSerializer.Serialize(expected, DefaultContext.IndexViewModel); + IndexViewModel obj = JsonSerializer.Deserialize(json, DefaultContext.IndexViewModel); VerifyIndexViewModel(expected, obj); } [Fact] - public static void RoundTripCampaignSummaryViewModel() + public virtual void RoundTripCampaignSummaryViewModel() { CampaignSummaryViewModel expected = CreateCampaignSummaryViewModel(); - string json = JsonSerializer.Serialize(expected, JsonContext.Default.CampaignSummaryViewModel); - CampaignSummaryViewModel obj = JsonSerializer.Deserialize(json, JsonContext.Default.CampaignSummaryViewModel); + string json = JsonSerializer.Serialize(expected, DefaultContext.CampaignSummaryViewModel); + CampaignSummaryViewModel obj = JsonSerializer.Deserialize(json, DefaultContext.CampaignSummaryViewModel); VerifyCampaignSummaryViewModel(expected, obj); } [Fact] - public static void RoundTripActiveOrUpcomingEvent() + public virtual void RoundTripActiveOrUpcomingEvent() { ActiveOrUpcomingEvent expected = CreateActiveOrUpcomingEvent(); - string json = JsonSerializer.Serialize(expected, JsonContext.Default.ActiveOrUpcomingEvent); - ActiveOrUpcomingEvent obj = JsonSerializer.Deserialize(json, JsonContext.Default.ActiveOrUpcomingEvent); + string json = JsonSerializer.Serialize(expected, DefaultContext.ActiveOrUpcomingEvent); + ActiveOrUpcomingEvent obj = JsonSerializer.Deserialize(json, DefaultContext.ActiveOrUpcomingEvent); VerifyActiveOrUpcomingEvent(expected, obj); } [Fact] - public static void RoundTripCollectionsDictionary() + public virtual void RoundTripCollectionsDictionary() { WeatherForecastWithPOCOs expected = CreateWeatherForecastWithPOCOs(); - string json = JsonSerializer.Serialize(expected, JsonContext.Default.WeatherForecastWithPOCOs); - WeatherForecastWithPOCOs obj = JsonSerializer.Deserialize(json, JsonContext.Default.WeatherForecastWithPOCOs); + string json = JsonSerializer.Serialize(expected, DefaultContext.WeatherForecastWithPOCOs); + WeatherForecastWithPOCOs obj = JsonSerializer.Deserialize(json, DefaultContext.WeatherForecastWithPOCOs); VerifyWeatherForecastWithPOCOs(expected, obj); } [Fact] - public static void RoundTripEmptyPoco() + public virtual void RoundTripEmptyPoco() { EmptyPoco expected = CreateEmptyPoco(); - string json = JsonSerializer.Serialize(expected, JsonContext.Default.EmptyPoco); - EmptyPoco obj = JsonSerializer.Deserialize(json, JsonContext.Default.EmptyPoco); + string json = JsonSerializer.Serialize(expected, DefaultContext.EmptyPoco); + EmptyPoco obj = JsonSerializer.Deserialize(json, DefaultContext.EmptyPoco); VerifyEmptyPoco(expected, obj); } [Fact] - public static void RoundTripTypeNameClash() + public virtual void RoundTripTypeNameClash() { RepeatedTypes.Location expected = CreateRepeatedLocation(); - string json = JsonSerializer.Serialize(expected, JsonContext.Default.RepeatedLocation); - RepeatedTypes.Location obj = JsonSerializer.Deserialize(json, JsonContext.Default.RepeatedLocation); + string json = JsonSerializer.Serialize(expected, DefaultContext.RepeatedLocation); + RepeatedTypes.Location obj = JsonSerializer.Deserialize(json, DefaultContext.RepeatedLocation); VerifyRepeatedLocation(expected, obj); } - private static Location CreateLocation() + protected static Location CreateLocation() { return new Location { @@ -112,7 +116,7 @@ namespace System.Text.Json.SourceGeneration.Tests }; } - private static void VerifyLocation(Location expected, Location obj) + protected static void VerifyLocation(Location expected, Location obj) { Assert.Equal(expected.Address1, obj.Address1); Assert.Equal(expected.Address2, obj.Address2); @@ -124,7 +128,7 @@ namespace System.Text.Json.SourceGeneration.Tests Assert.Equal(expected.Country, obj.Country); } - private static ActiveOrUpcomingEvent CreateActiveOrUpcomingEvent() + protected static ActiveOrUpcomingEvent CreateActiveOrUpcomingEvent() { return new ActiveOrUpcomingEvent { @@ -139,7 +143,7 @@ namespace System.Text.Json.SourceGeneration.Tests }; } - private static void VerifyActiveOrUpcomingEvent(ActiveOrUpcomingEvent expected, ActiveOrUpcomingEvent obj) + protected static void VerifyActiveOrUpcomingEvent(ActiveOrUpcomingEvent expected, ActiveOrUpcomingEvent obj) { Assert.Equal(expected.CampaignManagedOrganizerName, obj.CampaignManagedOrganizerName); Assert.Equal(expected.CampaignName, obj.CampaignName); @@ -151,7 +155,7 @@ namespace System.Text.Json.SourceGeneration.Tests Assert.Equal(expected.StartDate, obj.StartDate); } - private static CampaignSummaryViewModel CreateCampaignSummaryViewModel() + protected static CampaignSummaryViewModel CreateCampaignSummaryViewModel() { return new CampaignSummaryViewModel { @@ -164,7 +168,7 @@ namespace System.Text.Json.SourceGeneration.Tests }; } - private static void VerifyCampaignSummaryViewModel(CampaignSummaryViewModel expected, CampaignSummaryViewModel obj) + protected static void VerifyCampaignSummaryViewModel(CampaignSummaryViewModel expected, CampaignSummaryViewModel obj) { Assert.Equal(expected.Description, obj.Description); Assert.Equal(expected.Headline, obj.Headline); @@ -174,7 +178,7 @@ namespace System.Text.Json.SourceGeneration.Tests Assert.Equal(expected.Title, obj.Title); } - private static IndexViewModel CreateIndexViewModel() + protected static IndexViewModel CreateIndexViewModel() { return new IndexViewModel { @@ -204,7 +208,7 @@ namespace System.Text.Json.SourceGeneration.Tests }; } - private static void VerifyIndexViewModel(IndexViewModel expected, IndexViewModel obj) + protected static void VerifyIndexViewModel(IndexViewModel expected, IndexViewModel obj) { Assert.Equal(expected.ActiveOrUpcomingEvents.Count, obj.ActiveOrUpcomingEvents.Count); for (int i = 0; i < expected.ActiveOrUpcomingEvents.Count; i++) @@ -217,7 +221,7 @@ namespace System.Text.Json.SourceGeneration.Tests Assert.Equal(expected.IsNewAccount, obj.IsNewAccount); } - private static WeatherForecastWithPOCOs CreateWeatherForecastWithPOCOs() + protected static WeatherForecastWithPOCOs CreateWeatherForecastWithPOCOs() { return new WeatherForecastWithPOCOs { @@ -251,7 +255,7 @@ namespace System.Text.Json.SourceGeneration.Tests }; } - private static void VerifyWeatherForecastWithPOCOs(WeatherForecastWithPOCOs expected, WeatherForecastWithPOCOs obj) + protected static void VerifyWeatherForecastWithPOCOs(WeatherForecastWithPOCOs expected, WeatherForecastWithPOCOs obj) { Assert.Equal(expected.Date, obj.Date); Assert.Equal(expected.TemperatureCelsius, obj.TemperatureCelsius); @@ -277,7 +281,7 @@ namespace System.Text.Json.SourceGeneration.Tests } } - private static RepeatedTypes.Location CreateRepeatedLocation() + protected static RepeatedTypes.Location CreateRepeatedLocation() { return new RepeatedTypes.Location { @@ -293,7 +297,7 @@ namespace System.Text.Json.SourceGeneration.Tests }; } - private static void VerifyRepeatedLocation(RepeatedTypes.Location expected, RepeatedTypes.Location obj) + protected static void VerifyRepeatedLocation(RepeatedTypes.Location expected, RepeatedTypes.Location obj) { Assert.Equal(expected.FakeAddress1, obj.FakeAddress1); Assert.Equal(expected.FakeAddress2, obj.FakeAddress2); @@ -305,51 +309,51 @@ namespace System.Text.Json.SourceGeneration.Tests Assert.Equal(expected.FakeCountry, obj.FakeCountry); } - private static EmptyPoco CreateEmptyPoco() => new EmptyPoco(); + protected static EmptyPoco CreateEmptyPoco() => new EmptyPoco(); - private static void VerifyEmptyPoco(EmptyPoco expected, EmptyPoco obj) + protected static void VerifyEmptyPoco(EmptyPoco expected, EmptyPoco obj) { Assert.NotNull(expected); Assert.NotNull(obj); } [Fact] - public static void NestedSameTypeWorks() + public virtual void NestedSameTypeWorks() { MyType myType = new() { Type = new() }; - string json = JsonSerializer.Serialize(myType, JsonContext.Default.MyType); - myType = JsonSerializer.Deserialize(json, JsonContext.Default.MyType); - Assert.Equal(json, JsonSerializer.Serialize(myType, JsonContext.Default.MyType)); + string json = JsonSerializer.Serialize(myType, DefaultContext.MyType); + myType = JsonSerializer.Deserialize(json, DefaultContext.MyType); + Assert.Equal(json, JsonSerializer.Serialize(myType, DefaultContext.MyType)); MyType2 myType2 = new() { Type = new MyIntermediateType() { Type = myType } }; - json = JsonSerializer.Serialize(myType2, JsonContext.Default.MyType2); - myType2 = JsonSerializer.Deserialize(json, JsonContext.Default.MyType2); - Assert.Equal(json, JsonSerializer.Serialize(myType2, JsonContext.Default.MyType2)); + json = JsonSerializer.Serialize(myType2, DefaultContext.MyType2); + myType2 = JsonSerializer.Deserialize(json, DefaultContext.MyType2); + Assert.Equal(json, JsonSerializer.Serialize(myType2, DefaultContext.MyType2)); } [Fact] - public static void SerializeObjectArray() + public virtual void SerializeObjectArray() { IndexViewModel index = CreateIndexViewModel(); CampaignSummaryViewModel campaignSummary = CreateCampaignSummaryViewModel(); - string json = JsonSerializer.Serialize(new object[] { index, campaignSummary }, JsonContext.Default.ObjectArray); - object[] arr = JsonSerializer.Deserialize(json, JsonContext.Default.ObjectArray); + string json = JsonSerializer.Serialize(new object[] { index, campaignSummary }, DefaultContext.ObjectArray); + object[] arr = JsonSerializer.Deserialize(json, DefaultContext.ObjectArray); JsonElement indexAsJsonElement = (JsonElement)arr[0]; JsonElement campaignSummeryAsJsonElement = (JsonElement)arr[1]; - VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), JsonContext.Default.IndexViewModel)); - VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), JsonContext.Default.CampaignSummaryViewModel)); + VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), DefaultContext.IndexViewModel)); + VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), DefaultContext.CampaignSummaryViewModel)); } [Fact] - public static void SerializeObjectArray_WithCustomOptions() + public virtual void SerializeObjectArray_WithCustomOptions() { IndexViewModel index = CreateIndexViewModel(); CampaignSummaryViewModel campaignSummary = CreateCampaignSummaryViewModel(); JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - JsonContext context = new(options); + ITestContext context = _contextCreator(options); string json = JsonSerializer.Serialize(new object[] { index, campaignSummary }, context.ObjectArray); object[] arr = JsonSerializer.Deserialize(json, context.ObjectArray); @@ -361,13 +365,13 @@ namespace System.Text.Json.SourceGeneration.Tests } [Fact] - public static void SerializeObjectArray_SimpleTypes_WithCustomOptions() + public virtual void SerializeObjectArray_SimpleTypes_WithCustomOptions() { JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - JsonContext context = new JsonContext(options); + ITestContext context = _contextCreator(options); - string json = JsonSerializer.Serialize(new object[] { "Hello", "World" }, typeof(object[]), context); - object[] arr = (object[])JsonSerializer.Deserialize(json, typeof(object[]), context); + string json = JsonSerializer.Serialize(new object[] { "Hello", "World" }, typeof(object[]), (JsonSerializerContext)context); + object[] arr = (object[])JsonSerializer.Deserialize(json, typeof(object[]), (JsonSerializerContext)context); JsonElement hello = (JsonElement)arr[0]; JsonElement world = (JsonElement)arr[1]; @@ -376,16 +380,16 @@ namespace System.Text.Json.SourceGeneration.Tests } [Fact] - public static void HandlesNestedTypes() + public virtual void HandlesNestedTypes() { string json = @"{""MyInt"":5}"; - MyNestedClass obj = JsonSerializer.Deserialize(json, JsonContext.Default.MyNestedClass); + MyNestedClass obj = JsonSerializer.Deserialize(json, DefaultContext.MyNestedClass); Assert.Equal(5, obj.MyInt); - Assert.Equal(json, JsonSerializer.Serialize(obj, JsonContext.Default.MyNestedClass)); + Assert.Equal(json, JsonSerializer.Serialize(obj, DefaultContext.MyNestedClass)); - MyNestedClass.MyNestedNestedClass obj2 = JsonSerializer.Deserialize(json, JsonContext.Default.MyNestedNestedClass); + MyNestedClass.MyNestedNestedClass obj2 = JsonSerializer.Deserialize(json, DefaultContext.MyNestedNestedClass); Assert.Equal(5, obj2.MyInt); - Assert.Equal(json, JsonSerializer.Serialize(obj2, JsonContext.Default.MyNestedNestedClass)); + Assert.Equal(json, JsonSerializer.Serialize(obj2, DefaultContext.MyNestedNestedClass)); } public class MyNestedClass @@ -399,7 +403,7 @@ namespace System.Text.Json.SourceGeneration.Tests } [Fact] - public static void ConstructingFromOptionsKeepsReference() + public void ConstructingFromOptionsKeepsReference() { JsonStringEnumConverter converter = new(); JsonSerializerOptions options = new() @@ -408,29 +412,29 @@ namespace System.Text.Json.SourceGeneration.Tests Converters = { converter } }; - JsonContext context = new(options); + JsonSerializerContext context = (JsonSerializerContext)_contextCreator(options); Assert.Same(options, context.Options); Assert.Equal(options.PropertyNameCaseInsensitive, context.Options.PropertyNameCaseInsensitive); Assert.Same(converter, context.Options.Converters[0]); } [Fact] - public static void JsonContextDefaultClonesDefaultOptions() + public void JsonContextDefaultClonesDefaultOptions() { - JsonContext context = JsonContext.Default; + JsonSerializerContext context = (JsonSerializerContext)DefaultContext; Assert.Equal(0, context.Options.Converters.Count); } [Fact] - public static void JsonContextOptionsNotMutableAfterConstruction() + public void JsonContextOptionsNotMutableAfterConstruction() { - JsonContext context = JsonContext.Default; + JsonSerializerContext context = (JsonSerializerContext)DefaultContext; InvalidOperationException ex = Assert.Throws(() => context.Options.PropertyNameCaseInsensitive = true); string exAsStr = ex.ToString(); Assert.Contains("JsonSerializerOptions", exAsStr); Assert.Contains("JsonSerializerContext", exAsStr); - context = new JsonContext(new JsonSerializerOptions()); + context = (JsonSerializerContext)_contextCreator(new JsonSerializerOptions()); ex = Assert.Throws(() => context.Options.PropertyNameCaseInsensitive = true); exAsStr = ex.ToString(); Assert.Contains("JsonSerializerOptions", exAsStr); @@ -438,26 +442,26 @@ namespace System.Text.Json.SourceGeneration.Tests } [Fact] - public static void ParameterizedConstructor() + public virtual void ParameterizedConstructor() { - string json = JsonSerializer.Serialize(new HighLowTempsImmutable(1, 2), JsonContext.Default.HighLowTempsImmutable); + string json = JsonSerializer.Serialize(new HighLowTempsImmutable(1, 2), DefaultContext.HighLowTempsImmutable); Assert.Contains(@"""High"":1", json); Assert.Contains(@"""Low"":2", json); // Deserialization not supported for now. - Assert.Throws(() => JsonSerializer.Deserialize(json, JsonContext.Default.HighLowTempsImmutable)); + Assert.Throws(() => JsonSerializer.Deserialize(json, DefaultContext.HighLowTempsImmutable)); } [Fact] - public static void EnumAndNullable() + public virtual void EnumAndNullable() { RunTest(new ClassWithEnumAndNullable() { Day = DayOfWeek.Monday, NullableDay = DayOfWeek.Tuesday }); RunTest(new ClassWithEnumAndNullable()); - static void RunTest(ClassWithEnumAndNullable expected) + void RunTest(ClassWithEnumAndNullable expected) { - string json = JsonSerializer.Serialize(expected, JsonContext.Default.ClassWithEnumAndNullable); - ClassWithEnumAndNullable actual = JsonSerializer.Deserialize(json, JsonContext.Default.ClassWithEnumAndNullable); + string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithEnumAndNullable); + ClassWithEnumAndNullable actual = JsonSerializer.Deserialize(json, DefaultContext.ClassWithEnumAndNullable); Assert.Equal(expected.Day, actual.Day); Assert.Equal(expected.NullableDay, actual.NullableDay); } @@ -470,13 +474,13 @@ namespace System.Text.Json.SourceGeneration.Tests } [Fact] - public static void Converters_AndTypeInfoCreator_NotRooted_WhenMetadataNotPresent() + public void Converters_AndTypeInfoCreator_NotRooted_WhenMetadataNotPresent() { object[] objArr = new object[] { new MyStruct() }; // Metadata not generated for MyStruct without JsonSerializableAttribute. NotSupportedException ex = Assert.Throws( - () => JsonSerializer.Serialize(objArr, JsonContext.Default.ObjectArray)); + () => JsonSerializer.Serialize(objArr, DefaultContext.ObjectArray)); string exAsStr = ex.ToString(); Assert.Contains(typeof(MyStruct).ToString(), exAsStr); Assert.Contains("JsonSerializerOptions", exAsStr); @@ -493,7 +497,7 @@ namespace System.Text.Json.SourceGeneration.Tests AssertFieldNull("s_defaultFactoryConverters", optionsInstance: null); // Confirm type info dynamic creator not set. - AssertFieldNull("_typeInfoCreationFunc", JsonContext.Default.Options); + AssertFieldNull("_typeInfoCreationFunc", ((JsonSerializerContext)DefaultContext).Options); static void AssertFieldNull(string fieldName, JsonSerializerOptions? optionsInstance) { @@ -507,7 +511,7 @@ namespace System.Text.Json.SourceGeneration.Tests private const string ExceptionMessageFromCustomContext = "Exception thrown from custom context."; [Fact] - public static void GetTypeInfoCalledDuringPolymorphicSerialization() + public void GetTypeInfoCalledDuringPolymorphicSerialization() { CustomContext context = new(new JsonSerializerOptions()); @@ -529,13 +533,13 @@ namespace System.Text.Json.SourceGeneration.Tests internal class CustomContext : JsonSerializerContext { - public CustomContext(JsonSerializerOptions options) : base(options) { } + public CustomContext(JsonSerializerOptions options) : base(options, null) { } private JsonTypeInfo _object; public JsonTypeInfo Object => _object ??= JsonMetadataServices.CreateValueInfo(Options, JsonMetadataServices.ObjectConverter); private JsonTypeInfo _objectArray; - public JsonTypeInfo ObjectArray => _objectArray ??= JsonMetadataServices.CreateArrayInfo(Options, Object, default); + public JsonTypeInfo ObjectArray => _objectArray ??= JsonMetadataServices.CreateArrayInfo(Options, Object, default, serializeFunc: null); public override JsonTypeInfo GetTypeInfo(Type type) { @@ -547,5 +551,15 @@ namespace System.Text.Json.SourceGeneration.Tests throw new InvalidOperationException(ExceptionMessageFromCustomContext); } } + + protected static void AssertFastPathLogicCorrect(string expectedJson, T value, JsonTypeInfo typeInfo) + { + using MemoryStream ms = new(); + using Utf8JsonWriter writer = new(ms); + typeInfo.Serialize!(writer, value); + writer.Flush(); + + JsonTestHelper.AssertJsonEqual(expectedJson, Encoding.UTF8.GetString(ms.ToArray())); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs new file mode 100644 index 0000000..45ce3f3 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -0,0 +1,284 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + [JsonSerializable(typeof(Location), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RepeatedTypes.Location), GenerationMode = JsonSourceGenerationMode.Serialization, TypeInfoPropertyName = "RepeatedLocation")] + [JsonSerializable(typeof(ActiveOrUpcomingEvent), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(CampaignSummaryViewModel), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(IndexViewModel), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(WeatherForecastWithPOCOs), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(EmptyPoco), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] + internal partial class SerializationContext : JsonSerializerContext, ITestContext + { + } + + [JsonSerializerOptions(NamingPolicy = JsonKnownNamingPolicy.BuiltInCamelCase)] + [JsonSerializable(typeof(Location), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RepeatedTypes.Location), GenerationMode = JsonSourceGenerationMode.Serialization, TypeInfoPropertyName = "RepeatedLocation")] + [JsonSerializable(typeof(ActiveOrUpcomingEvent), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(CampaignSummaryViewModel), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(IndexViewModel), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(WeatherForecastWithPOCOs), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(EmptyPoco), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] + internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext + { + } + + public sealed class SerializationContextTests : RealWorldContextTests + { + public SerializationContextTests() : base(SerializationContext.Default, (options) => new SerializationContext(options)) { } + + [Fact] + public override void EnsureFastPathGeneratedAsExpected() + { + Assert.NotNull(SerializationContext.Default.Location.Serialize); + Assert.NotNull(SerializationContext.Default.RepeatedLocation.Serialize); + Assert.NotNull(SerializationContext.Default.ActiveOrUpcomingEvent.Serialize); + Assert.NotNull(SerializationContext.Default.CampaignSummaryViewModel.Serialize); + Assert.NotNull(SerializationContext.Default.IndexViewModel.Serialize); + Assert.NotNull(SerializationContext.Default.WeatherForecastWithPOCOs.Serialize); + Assert.NotNull(SerializationContext.Default.WeatherForecastWithPOCOs.Serialize); + Assert.NotNull(SerializationContext.Default.HighLowTemps.Serialize); + Assert.NotNull(SerializationContext.Default.MyType.Serialize); + Assert.NotNull(SerializationContext.Default.MyType2.Serialize); + Assert.NotNull(SerializationContext.Default.MyIntermediateType.Serialize); + Assert.NotNull(SerializationContext.Default.HighLowTempsImmutable.Serialize); + Assert.NotNull(SerializationContext.Default.MyNestedClass.Serialize); + Assert.NotNull(SerializationContext.Default.MyNestedNestedClass.Serialize); + Assert.Null(SerializationContext.Default.ObjectArray.Serialize); + Assert.Null(SerializationContext.Default.String.Serialize); + Assert.NotNull(SerializationContext.Default.ClassWithEnumAndNullable.Serialize); + } + + [Fact] + public override void RoundTripLocation() + { + Location expected = CreateLocation(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.Location); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.Location), typeof(Location)); + + Location obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).Location); + VerifyLocation(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.Location); + } + + [Fact] + public override void RoundTripIndexViewModel() + { + IndexViewModel expected = CreateIndexViewModel(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.IndexViewModel); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.IndexViewModel), typeof(IndexViewModel)); + + IndexViewModel obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).IndexViewModel); + VerifyIndexViewModel(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.IndexViewModel); + } + + [Fact] + public override void RoundTripCampaignSummaryViewModel() + { + CampaignSummaryViewModel expected = CreateCampaignSummaryViewModel(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.CampaignSummaryViewModel); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.CampaignSummaryViewModel), typeof(CampaignSummaryViewModel)); + + CampaignSummaryViewModel obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).CampaignSummaryViewModel); + VerifyCampaignSummaryViewModel(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.CampaignSummaryViewModel); + } + + [Fact] + public override void RoundTripActiveOrUpcomingEvent() + { + ActiveOrUpcomingEvent expected = CreateActiveOrUpcomingEvent(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.ActiveOrUpcomingEvent); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.ActiveOrUpcomingEvent), typeof(ActiveOrUpcomingEvent)); + + ActiveOrUpcomingEvent obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).ActiveOrUpcomingEvent); + VerifyActiveOrUpcomingEvent(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.ActiveOrUpcomingEvent); + } + + [Fact] + public override void RoundTripCollectionsDictionary() + { + WeatherForecastWithPOCOs expected = CreateWeatherForecastWithPOCOs(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.WeatherForecastWithPOCOs); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.WeatherForecastWithPOCOs), typeof(WeatherForecastWithPOCOs)); + + WeatherForecastWithPOCOs obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).WeatherForecastWithPOCOs); + VerifyWeatherForecastWithPOCOs(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.WeatherForecastWithPOCOs); + } + + [Fact] + public override void RoundTripEmptyPoco() + { + EmptyPoco expected = CreateEmptyPoco(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.EmptyPoco); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.EmptyPoco), typeof(EmptyPoco)); + + EmptyPoco obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).EmptyPoco); + VerifyEmptyPoco(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.EmptyPoco); + } + + [Fact] + public override void RoundTripTypeNameClash() + { + RepeatedTypes.Location expected = CreateRepeatedLocation(); + + string json = JsonSerializer.Serialize(expected, DefaultContext.RepeatedLocation); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.RepeatedLocation), typeof(RepeatedTypes.Location)); + + RepeatedTypes.Location obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).RepeatedLocation); + VerifyRepeatedLocation(expected, obj); + + AssertFastPathLogicCorrect(json, obj, DefaultContext.RepeatedLocation); + } + + [Fact] + public override void NestedSameTypeWorks() + { + MyType myType = new() { Type = new() }; + string json = JsonSerializer.Serialize(myType, DefaultContext.MyType); + myType = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).MyType); + AssertFastPathLogicCorrect(json, myType, DefaultContext.MyType); + + MyType2 myType2 = new() { Type = new MyIntermediateType() { Type = myType } }; + json = JsonSerializer.Serialize(myType2, DefaultContext.MyType2); + myType2 = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).MyType2); + AssertFastPathLogicCorrect(json, myType2, DefaultContext.MyType2); + } + + [Fact] + public override void SerializeObjectArray() + { + IndexViewModel index = CreateIndexViewModel(); + CampaignSummaryViewModel campaignSummary = CreateCampaignSummaryViewModel(); + + string json = JsonSerializer.Serialize(new object[] { index, campaignSummary }, DefaultContext.ObjectArray); + object[] arr = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).ObjectArray); + + JsonElement indexAsJsonElement = (JsonElement)arr[0]; + JsonElement campaignSummeryAsJsonElement = (JsonElement)arr[1]; + VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), ((ITestContext)MetadataContext.Default).IndexViewModel)); + VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), ((ITestContext)MetadataContext.Default).CampaignSummaryViewModel)); + } + + [Fact] + public override void SerializeObjectArray_WithCustomOptions() + { + IndexViewModel index = CreateIndexViewModel(); + CampaignSummaryViewModel campaignSummary = CreateCampaignSummaryViewModel(); + + ITestContext context = SerializationContextWithCamelCase.Default; + Assert.Same(JsonNamingPolicy.CamelCase, ((JsonSerializerContext)context).Options.PropertyNamingPolicy); + + string json = JsonSerializer.Serialize(new object[] { index, campaignSummary }, context.ObjectArray); + // Verify JSON was written with camel casing. + Assert.Contains("activeOrUpcomingEvents", json); + Assert.Contains("featuredCampaign", json); + Assert.Contains("description", json); + Assert.Contains("organizationName", json); + + object[] arr = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).ObjectArray); + + JsonElement indexAsJsonElement = (JsonElement)arr[0]; + JsonElement campaignSummeryAsJsonElement = (JsonElement)arr[1]; + + ITestContext metadataContext = new MetadataContext(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), metadataContext.IndexViewModel)); + VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), metadataContext.CampaignSummaryViewModel)); + } + + [Fact] + public override void SerializeObjectArray_SimpleTypes_WithCustomOptions() + { + JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + ITestContext context = new SerializationContext(options); + + string json = JsonSerializer.Serialize(new object[] { "Hello", "World" }, typeof(object[]), (JsonSerializerContext)context); + object[] arr = (object[])JsonSerializer.Deserialize(json, typeof(object[]), (JsonSerializerContext)((ITestContext)MetadataContext.Default)); + + JsonElement hello = (JsonElement)arr[0]; + JsonElement world = (JsonElement)arr[1]; + Assert.Equal("\"Hello\"", hello.GetRawText()); + Assert.Equal("\"World\"", world.GetRawText()); + } + + [Fact] + public override void HandlesNestedTypes() + { + string json = @"{""MyInt"":5}"; + MyNestedClass obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).MyNestedClass); + Assert.Equal(5, obj.MyInt); + Assert.Equal(json, JsonSerializer.Serialize(obj, DefaultContext.MyNestedClass)); + + MyNestedClass.MyNestedNestedClass obj2 = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).MyNestedNestedClass); + Assert.Equal(5, obj2.MyInt); + Assert.Equal(json, JsonSerializer.Serialize(obj2, DefaultContext.MyNestedNestedClass)); + } + + [Fact] + public override void EnumAndNullable() + { + RunTest(new ClassWithEnumAndNullable() { Day = DayOfWeek.Monday, NullableDay = DayOfWeek.Tuesday }); + RunTest(new ClassWithEnumAndNullable()); + + void RunTest(ClassWithEnumAndNullable expected) + { + string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithEnumAndNullable); + ClassWithEnumAndNullable actual = JsonSerializer.Deserialize(json, ((ITestContext)MetadataContext.Default).ClassWithEnumAndNullable); + Assert.Equal(expected.Day, actual.Day); + Assert.Equal(expected.NullableDay, actual.NullableDay); + } + } + + [Fact] + public override void ParameterizedConstructor() + { + string json = JsonSerializer.Serialize(new HighLowTempsImmutable(1, 2), DefaultContext.HighLowTempsImmutable); + Assert.Contains(@"""High"":1", json); + Assert.Contains(@"""Low"":2", json); + + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.HighLowTempsImmutable), typeof(HighLowTempsImmutable)); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationLogicTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationLogicTests.cs new file mode 100644 index 0000000..87d8822 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationLogicTests.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public static class SerializationLogicTests + { + [Theory] + [MemberData(nameof(GetOptionsUsingUnsupportedFeatures))] + [MemberData(nameof(GetIncompatibleOptions))] + public static void SerializationFuncNotInvokedWhenNotSupported(JsonSerializerOptions options) + { + JsonMessage message = new(); + + // Per context implementation, NotImplementedException thrown because the options are compatible, hence the serialization func is invoked. + Assert.Throws(() => JsonSerializer.Serialize(message, JsonContext.Default.JsonMessage)); + Assert.Throws(() => JsonSerializer.Serialize(message, typeof(JsonMessage), JsonContext.Default)); + + // NotSupportedException thrown because + // - the options are not compatible, hence the serialization func is not invoked. + // - the serializer correctly tries to serialize based on property metadata, but we have not provided it in our implementation. + JsonContext context = new(options); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Serialize(message, context.JsonMessage), typeof(JsonMessage)); + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Serialize(message, typeof(JsonMessage), context), typeof(JsonMessage)); + } + + [Fact] + public static void DictionaryFastPathPrimitiveValueSupported() + { + Assert.NotNull(DictionaryTypeContext.Default.DictionarySystemStringSystemString.Serialize); + Assert.NotNull(DictionaryTypeContext.Default.DictionarySystemStringSystemTextJsonSourceGenerationTestsJsonMessage.Serialize); + Assert.NotNull(DictionaryTypeContext.Default.JsonMessage.Serialize); + Assert.Null(DictionaryTypeContext.Default.String.Serialize); + Assert.Null(DictionaryTypeContext.Default.Int32.Serialize); + } + + // Options with features that aren't supported in generated serialization funcs. + public static IEnumerable GetOptionsUsingUnsupportedFeatures() + { + yield return new object[] { new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } } }; + yield return new object[] { new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping } }; + yield return new object[] { new JsonSerializerOptions { NumberHandling = JsonNumberHandling.WriteAsString } }; + yield return new object[] { new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve } }; + yield return new object[] { new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles } }; + yield return new object[] { new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles } }; + } + + // Options incompatible with JsonContext.s_defaultOptions below. + public static IEnumerable GetIncompatibleOptions() + { + yield return new object[] { new JsonSerializerOptions() }; + yield return new object[] { new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.Never } }; + yield return new object[] { new JsonSerializerOptions { IgnoreReadOnlyFields = true } }; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj index 09f2698..a88c018 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj @@ -9,8 +9,17 @@ + + + + + + + + + - + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index d5ebf88..0f1f6c6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -2,22 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Text.Json.Serialization; -using System.Text.Json.SourceGeneration.Tests; - -[assembly: JsonSerializable(typeof(Location))] -[assembly: JsonSerializable(typeof(System.Text.Json.SourceGeneration.Tests.RepeatedTypes.Location), TypeInfoPropertyName = "RepeatedLocation")] -[assembly: JsonSerializable(typeof(ActiveOrUpcomingEvent))] -[assembly: JsonSerializable(typeof(CampaignSummaryViewModel))] -[assembly: JsonSerializable(typeof(IndexViewModel))] -[assembly: JsonSerializable(typeof(WeatherForecastWithPOCOs))] -[assembly: JsonSerializable(typeof(EmptyPoco))] -// Ensure no errors when type of member in previously specified object graph is passed as input type to generator. -[assembly: JsonSerializable(typeof(HighLowTemps))] -[assembly: JsonSerializable(typeof(MyType))] -[assembly: JsonSerializable(typeof(MyType2))] -[assembly: JsonSerializable(typeof(MyIntermediateType))] -[assembly: JsonSerializable(typeof(HighLowTempsImmutable))] namespace System.Text.Json.SourceGeneration.Tests.RepeatedTypes { @@ -123,4 +107,10 @@ namespace System.Text.Json.SourceGeneration.Tests { public MyType Type = new(); } + + public class JsonMessage + { + public string Message { get; set; } + public int Length => Message?.Length ?? 0; // Read-only property + } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/CompilationHelper.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/CompilationHelper.cs index da43706..86a261f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/CompilationHelper.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/CompilationHelper.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using System.Text.Encodings.Web; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Xunit; @@ -33,6 +34,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests MetadataReference.CreateFromFile(typeof(Type).Assembly.Location), MetadataReference.CreateFromFile(typeof(KeyValuePair).Assembly.Location), MetadataReference.CreateFromFile(typeof(ContractNamespaceAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(JavaScriptEncoder).Assembly.Location), MetadataReference.CreateFromFile(systemRuntimeAssemblyPath), MetadataReference.CreateFromFile(systemCollectionsAssemblyPath), }; @@ -167,8 +169,14 @@ namespace System.Text.Json.SourceGeneration.UnitTests using System.Collections.Generic; using System.Text.Json.Serialization; - [assembly: JsonSerializable(typeof(Fake.Location))] - [assembly: JsonSerializable(typeof(HelloWorld.Location))] + namespace JsonSourceGeneration + { + [JsonSerializable(typeof(Fake.Location))] + [JsonSerializable(typeof(HelloWorld.Location))] + internal partial class JsonContext : JsonSerializerContext + { + } + } namespace Fake { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorDiagnosticsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorDiagnosticsTests.cs index 046c401..caa283f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorDiagnosticsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorDiagnosticsTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Xunit; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs index f462ca0..654b413 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs @@ -17,10 +17,13 @@ namespace System.Text.Json.SourceGeneration.UnitTests string source = @" using System.Text.Json.Serialization; - [assembly: JsonSerializable(typeof(HelloWorld.MyType))] - namespace HelloWorld { + [JsonSerializable(typeof(HelloWorld.MyType))] + internal partial class JsonContext : JsonSerializerContext + { + } + public class MyType { public int PublicPropertyInt { get; set; } @@ -83,11 +86,14 @@ namespace System.Text.Json.SourceGeneration.UnitTests using System.Text.Json.Serialization; using ReferencedAssembly; - [assembly: JsonSerializable(typeof(HelloWorld.MyType))] - [assembly: JsonSerializable(typeof(ReferencedAssembly.Location))] - namespace HelloWorld { + [JsonSerializable(typeof(HelloWorld.MyType))] + [JsonSerializable(typeof(ReferencedAssembly.Location))] + internal partial class JsonContext : JsonSerializerContext + { + } + public class MyType { public int PublicPropertyInt { get; set; } @@ -165,15 +171,19 @@ namespace System.Text.Json.SourceGeneration.UnitTests using System.Text.Json.Serialization; using ReferencedAssembly; - using @JsonSerializable = System.Runtime.Serialization.ContractNamespaceAttribute; + using @JsonSerializable = System.Runtime.Serialization.CollectionDataContractAttribute ; using AliasedAttribute = System.Text.Json.Serialization.JsonSerializableAttribute; - [assembly: AliasedAttribute(typeof(HelloWorld.MyType))] - [assembly: AliasedAttribute(typeof(ReferencedAssembly.Location))] - [module: @JsonSerializable(""my namespace"")] - namespace HelloWorld { + + [AliasedAttribute(typeof(HelloWorld.MyType))] + [AliasedAttribute(typeof(ReferencedAssembly.Location))] + [@JsonSerializable] + internal partial class JsonContext : JsonSerializerContext + { + } + public class MyType { public int PublicPropertyInt { get; set; } @@ -245,12 +255,15 @@ namespace System.Text.Json.SourceGeneration.UnitTests string source = @"using System; using System.Text.Json.Serialization; -[assembly: JsonSerializable(typeof(int))] -[assembly: JsonSerializable(typeof(string), TypeInfoPropertyName = ""Str"")] - namespace System.Text.Json.Serialization { - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(string), TypeInfoPropertyName = ""Str"")] + internal partial class JsonContext : JsonSerializerContext + { + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class JsonSerializableAttribute : JsonAttribute { public string TypeInfoPropertyName { get; set; } @@ -341,11 +354,14 @@ namespace System.Text.Json.Serialization using System.Collections.Generic; using System.Text.Json.Serialization; using ReferencedAssembly; - - [assembly: JsonSerializable(typeof(HelloWorld.WeatherForecastWithPOCOs))] namespace HelloWorld { + [JsonSerializable(typeof(HelloWorld.WeatherForecastWithPOCOs))] + internal partial class JsonContext : JsonSerializerContext + { + } + public class WeatherForecastWithPOCOs { public DateTimeOffset Date { get; set; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs index e08ee5e..afcd88a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs @@ -46,11 +46,14 @@ namespace System.Text.Json.SourceGeneration.UnitTests using System.Text.Json.Serialization; using ReferencedAssembly; - [assembly: JsonSerializable(typeof(HelloWorld.MyType))] - [assembly: JsonSerializable(typeof(ReferencedAssembly.ReferencedType))] - namespace HelloWorld { + [JsonSerializable(typeof(HelloWorld.MyType))] + [JsonSerializable(typeof(ReferencedAssembly.ReferencedType))] + internal partial class JsonContext : JsonSerializerContext + { + } + public class MyType { public void MyMethod() { } @@ -82,10 +85,13 @@ namespace System.Text.Json.SourceGeneration.UnitTests using System; using System.Text.Json.Serialization; - [assembly: JsonSerializable(typeof(HelloWorld.MyType))] - namespace HelloWorld { + [JsonSerializable(typeof(HelloWorld.MyType))] + internal partial class JsonContext : JsonSerializerContext + { + } + public class MyType { [JsonInclude] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonTestHelper.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonTestHelper.cs index f7f2aa2..338445a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonTestHelper.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonTestHelper.cs @@ -15,7 +15,7 @@ using Xunit.Sdk; namespace System.Text.Json { - internal static class JsonTestHelper + internal static partial class JsonTestHelper { #if BUILDING_INBOX_LIBRARY public const string DoubleFormatString = null; @@ -818,64 +818,5 @@ namespace System.Text.Json => s_replaceNewlines ? value.Replace(CompiledNewline, Environment.NewLine) : value; - - public static void AssertJsonEqual(string expected, string actual) - { - using JsonDocument expectedDom = JsonDocument.Parse(expected); - using JsonDocument actualDom = JsonDocument.Parse(actual); - AssertJsonEqual(expectedDom.RootElement, actualDom.RootElement); - } - - private static void AssertJsonEqual(JsonElement expected, JsonElement actual) - { - JsonValueKind valueKind = expected.ValueKind; - Assert.Equal(valueKind, actual.ValueKind); - - switch (valueKind) - { - case JsonValueKind.Object: - var propertyNames = new HashSet(); - - foreach (JsonProperty property in expected.EnumerateObject()) - { - propertyNames.Add(property.Name); - } - - foreach (JsonProperty property in actual.EnumerateObject()) - { - propertyNames.Add(property.Name); - } - - foreach (string name in propertyNames) - { - AssertJsonEqual(expected.GetProperty(name), actual.GetProperty(name)); - } - break; - case JsonValueKind.Array: - JsonElement.ArrayEnumerator expectedEnumerator = actual.EnumerateArray(); - JsonElement.ArrayEnumerator actualEnumerator = expected.EnumerateArray(); - - while (expectedEnumerator.MoveNext()) - { - Assert.True(actualEnumerator.MoveNext()); - AssertJsonEqual(expectedEnumerator.Current, actualEnumerator.Current); - } - - Assert.False(actualEnumerator.MoveNext()); - break; - case JsonValueKind.String: - Assert.Equal(expected.GetString(), actual.GetString()); - break; - case JsonValueKind.Number: - case JsonValueKind.True: - case JsonValueKind.False: - case JsonValueKind.Null: - Assert.Equal(expected.GetRawText(), actual.GetRawText()); - break; - default: - Debug.Fail($"Unexpected JsonValueKind: JsonValueKind.{valueKind}."); - break; - } - } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/Dictionary.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/Dictionary.cs index 781e0fa..b3e3bb3 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/Dictionary.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/Dictionary.cs @@ -23,7 +23,7 @@ namespace System.Text.Json.Tests.Serialization } else { - _Dictionary = JsonMetadataServices.CreateDictionaryInfo, string, HighLowTemps>(Options, () => new Dictionary(), this.String, this.HighLowTemps, default); + _Dictionary = JsonMetadataServices.CreateDictionaryInfo, string, HighLowTemps>(Options, () => new Dictionary(), this.String, this.HighLowTemps, default, serializeFunc: null); } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs index c477b08..39899a5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs @@ -22,15 +22,14 @@ namespace System.Text.Json.Tests.Serialization } else { - JsonTypeInfo objectInfo = JsonMetadataServices.CreateObjectInfo(); - _HighLowTemps = objectInfo; - - JsonMetadataServices.InitializeObjectInfo( - objectInfo, + JsonTypeInfo objectInfo = JsonMetadataServices.CreateObjectInfo( Options, createObjectFunc: static () => new HighLowTemps(), HighLowTempsPropInitFunc, - default); + default, + serializeFunc: null); + + _HighLowTemps = objectInfo; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/JsonContext.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/JsonContext.cs index f317268..c16add2 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/JsonContext.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/JsonContext.cs @@ -11,11 +11,11 @@ namespace System.Text.Json.Tests.Serialization private static JsonContext s_default; public static JsonContext Default => s_default ??= new JsonContext(new JsonSerializerOptions()); - public JsonContext() : base(null) + public JsonContext() : base(null, null) { } - public JsonContext(JsonSerializerOptions options) : base(options) + public JsonContext(JsonSerializerOptions options) : base(options, null) { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/List.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/List.cs index 24a2e95..3a09255 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/List.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/List.cs @@ -23,7 +23,7 @@ namespace System.Text.Json.Tests.Serialization } else { - _ListSystemDateTimeOffset = JsonMetadataServices.CreateListInfo, DateTimeOffset>(Options, () => new List(), this.DateTimeOffset, default); + _ListSystemDateTimeOffset = JsonMetadataServices.CreateListInfo, DateTimeOffset>(Options, () => new List(), this.DateTimeOffset, default, serializeFunc: null); } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/StringArray.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/StringArray.cs index 21445e3..6fab57d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/StringArray.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/StringArray.cs @@ -22,7 +22,7 @@ namespace System.Text.Json.Tests.Serialization } else { - _StringArray = JsonMetadataServices.CreateArrayInfo(Options, this.String, default); + _StringArray = JsonMetadataServices.CreateArrayInfo(Options, this.String, default, serializeFunc: null); } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs index 62d8593..f75200b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs @@ -22,15 +22,14 @@ namespace System.Text.Json.Tests.Serialization } else { - JsonTypeInfo objectInfo = JsonMetadataServices.CreateObjectInfo(); - _WeatherForecastWithPOCOs = objectInfo; - - JsonMetadataServices.InitializeObjectInfo( - objectInfo, + JsonTypeInfo objectInfo = JsonMetadataServices.CreateObjectInfo( Options, createObjectFunc: static () => new WeatherForecastWithPOCOs(), WeatherForecastWithPOCOsPropInitFunc, - default); + default, + serializeFunc: null); + + _WeatherForecastWithPOCOs = objectInfo; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs index 40feb1b..227aab7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs @@ -106,43 +106,41 @@ namespace System.Text.Json.Tests.Serialization { JsonSerializerOptions options = new(); - JsonTypeInfo info = JsonMetadataServices.CreateObjectInfo(); - - // Null info - ArgumentNullException ane = Assert.Throws(() => JsonMetadataServices.InitializeObjectInfo( - info: null, - options: options, + // Null options + ArgumentNullException ane = Assert.Throws(() => JsonMetadataServices.CreateObjectInfo( + options: null, createObjectFunc: null, propInitFunc: (context) => Array.Empty(), - numberHandling: default)); - Assert.Contains("info", ane.ToString()); + numberHandling: default, + serializeFunc: null)); + Assert.Contains("options", ane.ToString()); - // Info is not for object converter strategy - ArgumentException ae = Assert.Throws(() => JsonMetadataServices.InitializeObjectInfo( - info: JsonMetadataServices.CreateValueInfo(options, new DerivedClassConverter()), - options: options, + // Null prop init func is fine if serialize func is provided. + JsonMetadataServices.CreateObjectInfo( + options, createObjectFunc: null, - propInitFunc: (context) => Array.Empty(), - numberHandling: default)); - Assert.Contains("info", ae.ToString()); + propInitFunc: null, + numberHandling: default, + serializeFunc: (writer, obj) => { }); - // Null options - ane = Assert.Throws(() => JsonMetadataServices.InitializeObjectInfo( - info: info, - options: null, + // Null serialize func is fine if prop init func is provided. + JsonMetadataServices.CreateObjectInfo( + options, createObjectFunc: null, propInitFunc: (context) => Array.Empty(), - numberHandling: default)); - Assert.Contains("options", ane.ToString()); + numberHandling: default, + serializeFunc: null); - // Null prop init func. - ane = Assert.Throws(() => JsonMetadataServices.InitializeObjectInfo( - info: info, - options: options, + // Null prop init func and serialize func + InvalidOperationException ioe = Assert.Throws(() => JsonMetadataServices.CreateObjectInfo( + options, createObjectFunc: null, propInitFunc: null, - numberHandling: default)); - Assert.Contains("propInitFunc", ane.ToString()); + numberHandling: default, + serializeFunc: null)); + string ioeAsStr = ioe.ToString(); + Assert.Contains("propInitFunc", ioeAsStr); + Assert.Contains("serializeFunc", ioeAsStr); } [Fact] @@ -170,14 +168,16 @@ namespace System.Text.Json.Tests.Serialization ArgumentNullException ane = Assert.Throws(() => JsonMetadataServices.CreateArrayInfo( options: null, elementInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), - numberHandling: default)); + numberHandling: default, + serializeFunc: null)); Assert.Contains("options", ane.ToString()); // Null element info ane = Assert.Throws(() => JsonMetadataServices.CreateArrayInfo( - options: options, + options, elementInfo: null, - numberHandling: default)); + numberHandling: default, + serializeFunc: null)); Assert.Contains("elementInfo", ane.ToString()); } @@ -191,7 +191,8 @@ namespace System.Text.Json.Tests.Serialization options: null, createObjectFunc: null, elementInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), - numberHandling: default)); + numberHandling: default, + serializeFunc: null)); Assert.Contains("options", ane.ToString()); // Null element info @@ -199,7 +200,8 @@ namespace System.Text.Json.Tests.Serialization options: options, createObjectFunc: null, elementInfo: null, - numberHandling: default)); + numberHandling: default, + serializeFunc: null)); Assert.Contains("elementInfo", ane.ToString()); } @@ -214,7 +216,8 @@ namespace System.Text.Json.Tests.Serialization createObjectFunc: null, keyInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.StringConverter), valueInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), - numberHandling: default)); + numberHandling: default, + serializeFunc: null)); Assert.Contains("options", ane.ToString()); // Null key info @@ -223,7 +226,8 @@ namespace System.Text.Json.Tests.Serialization createObjectFunc: null, keyInfo: null, valueInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), - numberHandling: default)); + numberHandling: default, + serializeFunc: null)); Assert.Contains("keyInfo", ane.ToString()); // Null value info @@ -232,7 +236,8 @@ namespace System.Text.Json.Tests.Serialization createObjectFunc: null, keyInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.StringConverter), valueInfo: null, - numberHandling: default)); + numberHandling: default, + serializeFunc: null)); Assert.Contains("valueInfo", ane.ToString()); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs index 285eb39..7ebb217 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs @@ -89,16 +89,16 @@ namespace System.Text.Json.Tests.Serialization private class MyJsonContext : JsonSerializerContext { - public MyJsonContext() : base(null) { } + public MyJsonContext() : base(null, null) { } - public MyJsonContext(JsonSerializerOptions options) : base(options) { } + public MyJsonContext(JsonSerializerOptions options) : base(options, null) { } public override JsonTypeInfo? GetTypeInfo(Type type) => throw new NotImplementedException(); } private class MyJsonContextThatSetsOptionsInParameterlessCtor : JsonSerializerContext { - public MyJsonContextThatSetsOptionsInParameterlessCtor() : base(new JsonSerializerOptions()) { } + public MyJsonContextThatSetsOptionsInParameterlessCtor() : base(new JsonSerializerOptions(), null) { } public override JsonTypeInfo? GetTypeInfo(Type type) => throw new NotImplementedException(); } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 62488ff..b4773ff 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -15,6 +15,7 @@ + -- 2.7.4