From: Eirik Tsarpalis Date: Fri, 14 Jul 2023 16:37:45 +0000 (+0100) Subject: Extend JsonSourceGenerationOptionsAttribute to have feature parity with JsonSerialize... X-Git-Tag: accepted/tizen/unified/riscv/20231226.055536~1038 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=acedeb273be7f0ec2c815eba08d3ed9b2c85bafc;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Extend JsonSourceGenerationOptionsAttribute to have feature parity with JsonSerializerOptions. (#88753) --- diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonCommentHandling.cs b/src/libraries/System.Text.Json/Common/JsonCommentHandling.cs similarity index 100% rename from src/libraries/System.Text.Json/src/System/Text/Json/JsonCommentHandling.cs rename to src/libraries/System.Text.Json/Common/JsonCommentHandling.cs diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerDefaults.cs b/src/libraries/System.Text.Json/Common/JsonSerializerDefaults.cs similarity index 100% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerDefaults.cs rename to src/libraries/System.Text.Json/Common/JsonSerializerDefaults.cs diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs index 45f11a4..3d9e164 100644 --- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs @@ -16,11 +16,58 @@ namespace System.Text.Json.Serialization sealed class JsonSourceGenerationOptionsAttribute : JsonAttribute { /// + /// Constructs a new instance. + /// + public JsonSourceGenerationOptionsAttribute() { } + + /// + /// Constructs a new instance with a predefined set of options determined by the specified . + /// + /// The to reason about. + /// Invalid parameter. + public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults) + { + // Constructor kept in sync with equivalent overload in JsonSerializerOptions + + if (defaults is JsonSerializerDefaults.Web) + { + PropertyNameCaseInsensitive = true; + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase; + NumberHandling = JsonNumberHandling.AllowReadingFromString; + } + else if (defaults is not JsonSerializerDefaults.General) + { + throw new ArgumentOutOfRangeException(nameof(defaults)); + } + } + + /// + /// Defines whether an extra comma at the end of a list of JSON values in an object or array + /// is allowed (and ignored) within the JSON payload being deserialized. + /// + public bool AllowTrailingCommas { get; set; } + + /// + /// Specifies a list of custom converter types to be used. + /// + public Type[]? Converters { get; set; } + + /// + /// The default buffer size in bytes used when creating temporary buffers. + /// + public int DefaultBufferSize { get; set; } + + /// /// Specifies the default ignore condition. /// public JsonIgnoreCondition DefaultIgnoreCondition { get; set; } /// + /// Specifies the policy used to convert a dictionary key to another format, such as camel-casing. + /// + public JsonKnownNamingPolicy DictionaryKeyPolicy { get; set; } + + /// /// Specifies whether to ignore read-only fields. /// public bool IgnoreReadOnlyFields { get; set; } @@ -36,11 +83,47 @@ namespace System.Text.Json.Serialization public bool IncludeFields { get; set; } /// + /// Gets or sets the maximum depth allowed when serializing or deserializing JSON, with the default (i.e. 0) indicating a max depth of 64. + /// + public int MaxDepth { get; set; } + + /// + /// Specifies how number types should be handled when serializing or deserializing. + /// + public JsonNumberHandling NumberHandling { get; set; } + + /// + /// Specifies preferred object creation handling for properties when deserializing JSON. + /// + public JsonObjectCreationHandling PreferredObjectCreationHandling { get; set; } + + /// + /// Determines whether a property name uses a case-insensitive comparison during deserialization. + /// + public bool PropertyNameCaseInsensitive { get; set; } + + /// /// Specifies a built-in naming polices to convert JSON property names with. /// public JsonKnownNamingPolicy PropertyNamingPolicy { get; set; } /// + /// Defines how JSON comments are handled during deserialization. + /// + public JsonCommentHandling ReadCommentHandling { get; set; } + + /// + /// Defines how deserializing a type declared as an is handled during deserialization. + /// + public JsonUnknownTypeHandling UnknownTypeHandling { get; set; } + + /// + /// Determines how handles JSON properties that + /// cannot be mapped to a specific .NET member when deserializing object types. + /// + public JsonUnmappedMemberHandling UnmappedMemberHandling { get; set; } + + /// /// Specifies whether JSON output should be pretty-printed. /// public bool WriteIndented { get; set; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownTypeHandling.cs b/src/libraries/System.Text.Json/Common/JsonUnknownTypeHandling.cs similarity index 100% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownTypeHandling.cs rename to src/libraries/System.Text.Json/Common/JsonUnknownTypeHandling.cs diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index a6b61db..eca1f27 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -53,13 +53,16 @@ namespace System.Text.Json.SourceGeneration private const string JsonSerializerOptionsTypeRef = "global::System.Text.Json.JsonSerializerOptions"; private const string JsonSerializerContextTypeRef = "global::System.Text.Json.Serialization.JsonSerializerContext"; private const string Utf8JsonWriterTypeRef = "global::System.Text.Json.Utf8JsonWriter"; + private const string JsonCommentHandlingTypeRef = "global::System.Text.Json.JsonCommentHandling"; private const string JsonConverterTypeRef = "global::System.Text.Json.Serialization.JsonConverter"; private const string JsonConverterFactoryTypeRef = "global::System.Text.Json.Serialization.JsonConverterFactory"; private const string JsonCollectionInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues"; private const string JsonIgnoreConditionTypeRef = "global::System.Text.Json.Serialization.JsonIgnoreCondition"; + private const string JsonSerializerDefaultsTypeRef = "global::System.Text.Json.JsonSerializerDefaults"; private const string JsonNumberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonNumberHandling"; private const string JsonObjectCreationHandlingTypeRef = "global::System.Text.Json.Serialization.JsonObjectCreationHandling"; private const string JsonUnmappedMemberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonUnmappedMemberHandling"; + private const string JsonUnknownTypeHandlingTypeRef = "global::System.Text.Json.Serialization.JsonUnknownTypeHandling"; private const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices"; private const string JsonObjectInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues"; private const string JsonParameterInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues"; @@ -380,7 +383,7 @@ namespace System.Text.Json.SourceGeneration }; {{JsonTypeInfoLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{createCollectionMethodExpr}}; - {{JsonTypeInfoLocalVariableName}}.{{NumberHandlingPropName}} = {{GetNumberHandlingAsStr(typeGenerationSpec.NumberHandling)}}; + {{JsonTypeInfoLocalVariableName}}.{{NumberHandlingPropName}} = {{FormatNumberHandling(typeGenerationSpec.NumberHandling)}}; """); GenerateTypeInfoFactoryFooter(writer); @@ -533,7 +536,7 @@ namespace System.Text.Json.SourceGeneration }; {{JsonTypeInfoLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.CreateObjectInfo<{{typeMetadata.TypeRef.FullyQualifiedName}}>({{OptionsLocalVariableName}}, {{ObjectInfoVarName}}); - {{JsonTypeInfoLocalVariableName}}.{{NumberHandlingPropName}} = {{GetNumberHandlingAsStr(typeMetadata.NumberHandling)}}; + {{JsonTypeInfoLocalVariableName}}.{{NumberHandlingPropName}} = {{FormatNumberHandling(typeMetadata.NumberHandling)}}; """); if (typeMetadata is { UnmappedMemberHandling: not null } or { PreferredPropertyObjectCreationHandling: not null }) @@ -542,12 +545,12 @@ namespace System.Text.Json.SourceGeneration if (typeMetadata.UnmappedMemberHandling != null) { - writer.WriteLine($"{JsonTypeInfoLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)};"); + writer.WriteLine($"{JsonTypeInfoLocalVariableName}.{UnmappedMemberHandlingPropName} = {FormatUnmappedMemberHandling(typeMetadata.UnmappedMemberHandling.Value)};"); } if (typeMetadata.PreferredPropertyObjectCreationHandling != null) { - writer.WriteLine($"{JsonTypeInfoLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)};"); + writer.WriteLine($"{JsonTypeInfoLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {FormatObjectCreationHandling(typeMetadata.PreferredPropertyObjectCreationHandling.Value)};"); } } @@ -647,7 +650,7 @@ namespace System.Text.Json.SourceGeneration IgnoreCondition = {{ignoreConditionNamedArg}}, HasJsonInclude = {{FormatBool(property.HasJsonInclude)}}, IsExtensionData = {{FormatBool(property.IsExtensionData)}}, - NumberHandling = {{GetNumberHandlingAsStr(property.NumberHandling)}}, + NumberHandling = {{FormatNumberHandling(property.NumberHandling)}}, PropertyName = {{FormatStringLiteral(property.MemberName)}}, JsonPropertyName = {{FormatStringLiteral(property.JsonPropertyName)}} }; @@ -663,7 +666,7 @@ namespace System.Text.Json.SourceGeneration if (property.ObjectCreationHandling != null) { - writer.WriteLine($"properties[{i}].ObjectCreationHandling = {GetObjectCreationHandlingAsStr(property.ObjectCreationHandling.Value)};"); + writer.WriteLine($"properties[{i}].ObjectCreationHandling = {FormatObjectCreationHandling(property.ObjectCreationHandling.Value)};"); } writer.WriteLine(); @@ -769,12 +772,12 @@ namespace System.Text.Json.SourceGeneration continue; } - string runtimePropName = propertyGenSpec.RuntimePropertyName; - string propNameVarName = propertyGenSpec.PropertyNameVarName; + string effectiveJsonPropertyName = propertyGenSpec.EffectiveJsonPropertyName; + string propertyNameFieldName = propertyGenSpec.PropertyNameFieldName; // Add the property names to the context-wide cache; we'll generate the source to initialize them at the end of generation. - Debug.Assert(!_propertyNames.TryGetValue(runtimePropName, out string? existingName) || existingName == propNameVarName); - _propertyNames.TryAdd(runtimePropName, propNameVarName); + Debug.Assert(!_propertyNames.TryGetValue(effectiveJsonPropertyName, out string? existingName) || existingName == propertyNameFieldName); + _propertyNames.TryAdd(effectiveJsonPropertyName, propertyNameFieldName); DefaultCheckType defaultCheckType = GetDefaultCheckType(contextSpec, propertyGenSpec); @@ -812,7 +815,7 @@ namespace System.Text.Json.SourceGeneration break; } - GenerateSerializePropertyStatement(writer, propertyTypeSpec, propNameVarName, propValueExpr); + GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr); if (defaultCheckType != DefaultCheckType.None) { @@ -979,7 +982,7 @@ namespace System.Text.Json.SourceGeneration private static DefaultCheckType GetDefaultCheckType(ContextGenerationSpec contextSpec, PropertyGenerationSpec propertySpec) { - return (propertySpec.DefaultIgnoreCondition ?? contextSpec.DefaultIgnoreCondition) switch + return (propertySpec.DefaultIgnoreCondition ?? contextSpec.GeneratedOptionsSpec?.DefaultIgnoreCondition) switch { JsonIgnoreCondition.WhenWritingNull => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.None, JsonIgnoreCondition.WhenWritingDefault => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default, @@ -1040,7 +1043,7 @@ namespace System.Text.Json.SourceGeneration SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec, isPrimaryContextSourceFile: true); - GetLogicForDefaultSerializerOptionsInit(contextSpec, writer); + GetLogicForDefaultSerializerOptionsInit(contextSpec.GeneratedOptionsSpec, writer); writer.WriteLine(); @@ -1073,33 +1076,110 @@ namespace System.Text.Json.SourceGeneration return CompleteSourceFileAndReturnText(writer); } - private static void GetLogicForDefaultSerializerOptionsInit(ContextGenerationSpec contextSpec, SourceWriter writer) + private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOptionsSpec? optionsSpec, SourceWriter writer) { - string? namingPolicyName = contextSpec.PropertyNamingPolicy switch + const string DefaultOptionsFieldDecl = $"private readonly static {JsonSerializerOptionsTypeRef} {DefaultOptionsStaticVarName}"; + + if (optionsSpec is null) { - JsonKnownNamingPolicy.CamelCase => nameof(JsonNamingPolicy.CamelCase), - JsonKnownNamingPolicy.SnakeCaseLower => nameof(JsonNamingPolicy.SnakeCaseLower), - JsonKnownNamingPolicy.SnakeCaseUpper => nameof(JsonNamingPolicy.SnakeCaseUpper), - JsonKnownNamingPolicy.KebabCaseLower => nameof(JsonNamingPolicy.KebabCaseLower), - JsonKnownNamingPolicy.KebabCaseUpper => nameof(JsonNamingPolicy.KebabCaseUpper), - _ => null, - }; + writer.WriteLine($"{DefaultOptionsFieldDecl} = new();"); + return; + } - string namingPolicy = namingPolicyName != null - ? $"{JsonNamingPolicyTypeRef}.{namingPolicyName}" - : "null"; + if (optionsSpec.Defaults is JsonSerializerDefaults defaults) + { + writer.WriteLine($"{DefaultOptionsFieldDecl} = new({FormatJsonSerializerDefaults(defaults)})"); + } + else + { + writer.WriteLine($"{DefaultOptionsFieldDecl} = new()"); + } - writer.WriteLine($$""" - private readonly static {{JsonSerializerOptionsTypeRef}} {{DefaultOptionsStaticVarName}} = new() + writer.WriteLine('{'); + writer.Indentation++; + + if (optionsSpec.AllowTrailingCommas is bool allowTrailingCommas) + writer.WriteLine($"AllowTrailingCommas = {FormatBool(allowTrailingCommas)},"); + + if (optionsSpec.Converters is { Count: > 0 } converters) + { + writer.WriteLine("Converters ="); + writer.WriteLine('{'); + writer.Indentation++; + + foreach (TypeRef converter in converters) { - DefaultIgnoreCondition = {{JsonIgnoreConditionTypeRef}}.{{contextSpec.DefaultIgnoreCondition}}, - IgnoreReadOnlyFields = {{FormatBool(contextSpec.IgnoreReadOnlyFields)}}, - IgnoreReadOnlyProperties = {{FormatBool(contextSpec.IgnoreReadOnlyProperties)}}, - IncludeFields = {{FormatBool(contextSpec.IncludeFields)}}, - WriteIndented = {{FormatBool(contextSpec.WriteIndented)}}, - PropertyNamingPolicy = {{namingPolicy}} + writer.WriteLine($"new {converter.FullyQualifiedName}(),"); + } + + writer.Indentation--; + writer.WriteLine("},"); + } + + if (optionsSpec.DefaultBufferSize is int defaultBufferSize) + writer.WriteLine($"DefaultBufferSize = {defaultBufferSize},"); + + if (optionsSpec.DefaultIgnoreCondition is JsonIgnoreCondition defaultIgnoreCondition) + writer.WriteLine($"DefaultIgnoreCondition = {FormatIgnoreCondition(defaultIgnoreCondition)},"); + + if (optionsSpec.DictionaryKeyPolicy is JsonKnownNamingPolicy dictionaryKeyPolicy) + writer.WriteLine($"DictionaryKeyPolicy = {FormatNamingPolicy(dictionaryKeyPolicy)},"); + + if (optionsSpec.IgnoreReadOnlyFields is bool ignoreReadOnlyFields) + writer.WriteLine($"IgnoreReadOnlyFields = {FormatBool(ignoreReadOnlyFields)},"); + + if (optionsSpec.IgnoreReadOnlyProperties is bool ignoreReadOnlyProperties) + writer.WriteLine($"IgnoreReadOnlyProperties = {FormatBool(ignoreReadOnlyProperties)},"); + + if (optionsSpec.IncludeFields is bool includeFields) + writer.WriteLine($"IncludeFields = {FormatBool(includeFields)},"); + + if (optionsSpec.MaxDepth is int maxDepth) + writer.WriteLine($"MaxDepth = {maxDepth},"); + + if (optionsSpec.NumberHandling is JsonNumberHandling numberHandling) + writer.WriteLine($"NumberHandling = {FormatNumberHandling(numberHandling)},"); + + if (optionsSpec.PreferredObjectCreationHandling is JsonObjectCreationHandling preferredObjectCreationHandling) + writer.WriteLine($"PreferredObjectCreationHandling = {FormatObjectCreationHandling(preferredObjectCreationHandling)},"); + + if (optionsSpec.PropertyNameCaseInsensitive is bool propertyNameCaseInsensitive) + writer.WriteLine($"PropertyNameCaseInsensitive = {FormatBool(propertyNameCaseInsensitive)},"); + + if (optionsSpec.PropertyNamingPolicy is JsonKnownNamingPolicy propertyNamingPolicy) + writer.WriteLine($"PropertyNamingPolicy = {FormatNamingPolicy(propertyNamingPolicy)},"); + + if (optionsSpec.ReadCommentHandling is JsonCommentHandling readCommentHandling) + writer.WriteLine($"ReadCommentHandling = {FormatCommentHandling(readCommentHandling)},"); + + if (optionsSpec.UnknownTypeHandling is JsonUnknownTypeHandling unknownTypeHandling) + writer.WriteLine($"UnknownTypeHandling = {FormatUnknownTypeHandling(unknownTypeHandling)},"); + + if (optionsSpec.UnmappedMemberHandling is JsonUnmappedMemberHandling unmappedMemberHandling) + writer.WriteLine($"UnmappedMemberHandling = {FormatUnmappedMemberHandling(unmappedMemberHandling)},"); + + if (optionsSpec.WriteIndented is bool writeIndented) + writer.WriteLine($"WriteIndented = {FormatBool(writeIndented)},"); + + writer.Indentation--; + writer.WriteLine("};"); + + static string FormatNamingPolicy(JsonKnownNamingPolicy knownNamingPolicy) + { + string? policyName = knownNamingPolicy switch + { + JsonKnownNamingPolicy.CamelCase => nameof(JsonNamingPolicy.CamelCase), + JsonKnownNamingPolicy.SnakeCaseLower => nameof(JsonNamingPolicy.SnakeCaseLower), + JsonKnownNamingPolicy.SnakeCaseUpper => nameof(JsonNamingPolicy.SnakeCaseUpper), + JsonKnownNamingPolicy.KebabCaseLower => nameof(JsonNamingPolicy.KebabCaseLower), + JsonKnownNamingPolicy.KebabCaseUpper => nameof(JsonNamingPolicy.KebabCaseUpper), + _ => null, }; - """); + + return policyName != null + ? $"{JsonNamingPolicyTypeRef}.{policyName}" + : "null"; + } } private static void GenerateConverterHelpers(SourceWriter writer, bool emitGetConverterForNullablePropertyMethod) @@ -1230,17 +1310,29 @@ namespace System.Text.Json.SourceGeneration return CompleteSourceFileAndReturnText(writer); } - private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling) + private static string FormatNumberHandling(JsonNumberHandling? numberHandling) => numberHandling.HasValue ? SourceGeneratorHelpers.FormatEnumLiteral(JsonNumberHandlingTypeRef, numberHandling.Value) : "null"; - private static string GetObjectCreationHandlingAsStr(JsonObjectCreationHandling creationHandling) + private static string FormatObjectCreationHandling(JsonObjectCreationHandling creationHandling) => SourceGeneratorHelpers.FormatEnumLiteral(JsonObjectCreationHandlingTypeRef, creationHandling); - private static string GetUnmappedMemberHandlingAsStr(JsonUnmappedMemberHandling unmappedMemberHandling) + private static string FormatUnmappedMemberHandling(JsonUnmappedMemberHandling unmappedMemberHandling) => SourceGeneratorHelpers.FormatEnumLiteral(JsonUnmappedMemberHandlingTypeRef, unmappedMemberHandling); + private static string FormatCommentHandling(JsonCommentHandling commentHandling) + => SourceGeneratorHelpers.FormatEnumLiteral(JsonCommentHandlingTypeRef, commentHandling); + + private static string FormatUnknownTypeHandling(JsonUnknownTypeHandling commentHandling) + => SourceGeneratorHelpers.FormatEnumLiteral(JsonUnknownTypeHandlingTypeRef, commentHandling); + + private static string FormatIgnoreCondition(JsonIgnoreCondition ignoreCondition) + => SourceGeneratorHelpers.FormatEnumLiteral(JsonIgnoreConditionTypeRef, ignoreCondition); + + private static string FormatJsonSerializerDefaults(JsonSerializerDefaults defaults) + => SourceGeneratorHelpers.FormatEnumLiteral(JsonSerializerDefaultsTypeRef, defaults); + private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>"; private static string FormatBool(bool value) => value ? "true" : "false"; @@ -1403,19 +1495,19 @@ namespace System.Text.Json.SourceGeneration // Algorithm should be kept in sync with the reflection equivalent in JsonTypeInfo.cs string memberName = propertySpec.MemberName; - if (state.AddedProperties.TryAdd(propertySpec.RuntimePropertyName, (propertySpec, state.Properties.Count))) + if (state.AddedProperties.TryAdd(propertySpec.EffectiveJsonPropertyName, (propertySpec, state.Properties.Count))) { state.Properties.Add(propertySpec); } else { // The JsonPropertyNameAttribute or naming policy resulted in a collision. - (PropertyGenerationSpec other, int index) = state.AddedProperties[propertySpec.RuntimePropertyName]; + (PropertyGenerationSpec other, int index) = state.AddedProperties[propertySpec.EffectiveJsonPropertyName]; if (other.DefaultIgnoreCondition == JsonIgnoreCondition.Always) { // Overwrite previously cached property since it has [JsonIgnore]. - state.AddedProperties[propertySpec.RuntimePropertyName] = (propertySpec, index); + state.AddedProperties[propertySpec.EffectiveJsonPropertyName] = (propertySpec, index); state.Properties[index] = propertySpec; } else diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 423d340..b8bc986 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -94,7 +94,7 @@ namespace System.Text.Json.SourceGeneration if (!TryParseJsonSerializerContextAttributes( contextTypeSymbol, out List? rootSerializableTypes, - out JsonSourceGenerationOptionsAttribute? options)) + out SourceGenerationOptionsSpec? options)) { // Context does not specify any source gen attributes. return null; @@ -122,8 +122,6 @@ namespace System.Text.Json.SourceGeneration return null; } - options ??= new JsonSourceGenerationOptionsAttribute(); - // Enqueue attribute data for spec generation foreach (TypeToGenerate rootSerializableType in rootSerializableTypes) { @@ -150,12 +148,7 @@ namespace System.Text.Json.SourceGeneration GeneratedTypes = _generatedTypes.Values.OrderBy(t => t.TypeRef.FullyQualifiedName).ToImmutableEquatableArray(), Namespace = contextTypeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null, ContextClassDeclarations = classDeclarationList.ToImmutableEquatableArray(), - DefaultIgnoreCondition = options.DefaultIgnoreCondition, - IgnoreReadOnlyFields = options.IgnoreReadOnlyFields, - IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties, - IncludeFields = options.IncludeFields, - PropertyNamingPolicy = options.PropertyNamingPolicy, - WriteIndented = options.WriteIndented, + GeneratedOptionsSpec = options, }; // Clear the caches of generated metadata between the processing of context classes. @@ -224,9 +217,9 @@ namespace System.Text.Json.SourceGeneration } private bool TryParseJsonSerializerContextAttributes( - ITypeSymbol contextClassSymbol, + INamedTypeSymbol contextClassSymbol, out List? rootSerializableTypes, - out JsonSourceGenerationOptionsAttribute? options) + out SourceGenerationOptionsSpec? options) { Debug.Assert(_knownSymbols.JsonSerializableAttributeType != null); Debug.Assert(_knownSymbols.JsonSourceGenerationOptionsAttributeType != null); @@ -250,47 +243,125 @@ namespace System.Text.Json.SourceGeneration } else if (SymbolEqualityComparer.Default.Equals(attributeClass, _knownSymbols.JsonSourceGenerationOptionsAttributeType)) { - options = ParseJsonSourceGenerationOptionsAttribute(attributeData); + options = ParseJsonSourceGenerationOptionsAttribute(contextClassSymbol, attributeData); } } return rootSerializableTypes != null || options != null; } - private static JsonSourceGenerationOptionsAttribute ParseJsonSourceGenerationOptionsAttribute(AttributeData attributeData) + private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(INamedTypeSymbol contextType, AttributeData attributeData) { - JsonSourceGenerationOptionsAttribute options = new(); + JsonSourceGenerationMode? generationMode = null; + List? converters = null; + JsonSerializerDefaults? defaults = null; + bool? allowTrailingCommas = null; + int? defaultBufferSize = null; + JsonIgnoreCondition? defaultIgnoreCondition = null; + JsonKnownNamingPolicy? dictionaryKeyPolicy = null; + bool? ignoreReadOnlyFields = null; + bool? ignoreReadOnlyProperties = null; + bool? includeFields = null; + int? maxDepth = null; + JsonNumberHandling? numberHandling = null; + JsonObjectCreationHandling? preferredObjectCreationHandling = null; + bool? propertyNameCaseInsensitive = null; + JsonKnownNamingPolicy? propertyNamingPolicy = null; + JsonCommentHandling? readCommentHandling = null; + JsonUnknownTypeHandling? unknownTypeHandling = null; + JsonUnmappedMemberHandling? unmappedMemberHandling = null; + bool? writeIndented = null; + + if (attributeData.ConstructorArguments.Length > 0) + { + Debug.Assert(attributeData.ConstructorArguments.Length == 1 & attributeData.ConstructorArguments[0].Type?.Name is nameof(JsonSerializerDefaults)); + defaults = (JsonSerializerDefaults)attributeData.ConstructorArguments[0].Value!; + } foreach (KeyValuePair namedArg in attributeData.NamedArguments) { switch (namedArg.Key) { + case nameof(JsonSourceGenerationOptionsAttribute.AllowTrailingCommas): + allowTrailingCommas = (bool)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.Converters): + converters = new List(); + foreach (TypedConstant element in namedArg.Value.Values) + { + var converterType = (ITypeSymbol?)element.Value; + TypeRef? typeRef = GetConverterTypeFromAttribute(contextType, converterType, contextType, attributeData); + if (typeRef != null) + { + converters.Add(typeRef); + } + } + + break; + + case nameof(JsonSourceGenerationOptionsAttribute.DefaultBufferSize): + defaultBufferSize = (int)namedArg.Value.Value!; + break; + case nameof(JsonSourceGenerationOptionsAttribute.DefaultIgnoreCondition): - options.DefaultIgnoreCondition = (JsonIgnoreCondition)namedArg.Value.Value!; + defaultIgnoreCondition = (JsonIgnoreCondition)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.DictionaryKeyPolicy): + dictionaryKeyPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!; break; case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyFields): - options.IgnoreReadOnlyFields = (bool)namedArg.Value.Value!; + ignoreReadOnlyFields = (bool)namedArg.Value.Value!; break; case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyProperties): - options.IgnoreReadOnlyProperties = (bool)namedArg.Value.Value!; + ignoreReadOnlyProperties = (bool)namedArg.Value.Value!; break; case nameof(JsonSourceGenerationOptionsAttribute.IncludeFields): - options.IncludeFields = (bool)namedArg.Value.Value!; + includeFields = (bool)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.MaxDepth): + maxDepth = (int)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.NumberHandling): + numberHandling = (JsonNumberHandling)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.PreferredObjectCreationHandling): + preferredObjectCreationHandling = (JsonObjectCreationHandling)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.PropertyNameCaseInsensitive): + propertyNameCaseInsensitive = (bool)namedArg.Value.Value!; break; case nameof(JsonSourceGenerationOptionsAttribute.PropertyNamingPolicy): - options.PropertyNamingPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!; + propertyNamingPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.ReadCommentHandling): + readCommentHandling = (JsonCommentHandling)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.UnknownTypeHandling): + unknownTypeHandling = (JsonUnknownTypeHandling)namedArg.Value.Value!; + break; + + case nameof(JsonSourceGenerationOptionsAttribute.UnmappedMemberHandling): + unmappedMemberHandling = (JsonUnmappedMemberHandling)namedArg.Value.Value!; break; case nameof(JsonSourceGenerationOptionsAttribute.WriteIndented): - options.WriteIndented = (bool)namedArg.Value.Value!; + writeIndented = (bool)namedArg.Value.Value!; break; case nameof(JsonSourceGenerationOptionsAttribute.GenerationMode): - options.GenerationMode = (JsonSourceGenerationMode)namedArg.Value.Value!; + generationMode = (JsonSourceGenerationMode)namedArg.Value.Value!; break; default: @@ -298,7 +369,28 @@ namespace System.Text.Json.SourceGeneration } } - return options; + return new SourceGenerationOptionsSpec + { + GenerationMode = generationMode, + Defaults = defaults, + AllowTrailingCommas = allowTrailingCommas, + DefaultBufferSize = defaultBufferSize, + Converters = converters?.ToImmutableEquatableArray(), + DefaultIgnoreCondition = defaultIgnoreCondition, + DictionaryKeyPolicy = dictionaryKeyPolicy, + IgnoreReadOnlyFields = ignoreReadOnlyFields, + IgnoreReadOnlyProperties = ignoreReadOnlyProperties, + IncludeFields = includeFields, + MaxDepth = maxDepth, + NumberHandling = numberHandling, + PreferredObjectCreationHandling = preferredObjectCreationHandling, + PropertyNameCaseInsensitive = propertyNameCaseInsensitive, + PropertyNamingPolicy = propertyNamingPolicy, + ReadCommentHandling = readCommentHandling, + UnknownTypeHandling = unknownTypeHandling, + UnmappedMemberHandling = unmappedMemberHandling, + WriteIndented = writeIndented, + }; } private static TypeToGenerate? ParseJsonSerializableAttribute(AttributeData attributeData) @@ -342,7 +434,7 @@ namespace System.Text.Json.SourceGeneration }; } - private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGenerate, INamedTypeSymbol contextType, Location contextLocation, JsonSourceGenerationOptionsAttribute options) + private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGenerate, INamedTypeSymbol contextType, Location contextLocation, SourceGenerationOptionsSpec? options) { Debug.Assert(IsSymbolAccessibleWithin(typeToGenerate.Type, within: contextType), "should not generate metadata for inaccessible types."); @@ -481,7 +573,7 @@ namespace System.Text.Json.SourceGeneration { TypeRef = typeRef, TypeInfoPropertyName = typeInfoPropertyName, - GenerationMode = typeToGenerate.Mode ?? options.GenerationMode, + GenerationMode = typeToGenerate.Mode ?? options?.GenerationMode ?? JsonSourceGenerationMode.Default, ClassType = classType, PrimitiveTypeKind = primitiveTypeKind, IsPolymorphic = isPolymorphic, @@ -546,7 +638,7 @@ namespace System.Text.Json.SourceGeneration } else if (!foundJsonConverterAttribute && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType)) { - customConverterType = GetConverterTypeFromAttribute(contextType, typeToGenerate.Type, attributeData); + customConverterType = GetConverterTypeFromJsonConverterAttribute(contextType, typeToGenerate.Type, attributeData); foundJsonConverterAttribute = true; } @@ -735,7 +827,7 @@ namespace System.Text.Json.SourceGeneration INamedTypeSymbol contextType, in TypeToGenerate typeToGenerate, Location typeLocation, - JsonSourceGenerationOptionsAttribute options, + SourceGenerationOptionsSpec? options, out bool hasExtensionDataProperty) { List properties = new(); @@ -845,7 +937,7 @@ namespace System.Text.Json.SourceGeneration ISymbol memberInfo, ref bool typeHasExtensionDataProperty, JsonSourceGenerationMode? generationMode, - JsonSourceGenerationOptionsAttribute options) + SourceGenerationOptionsSpec? options) { Debug.Assert(memberInfo is IFieldSymbol or IPropertySymbol); @@ -903,9 +995,8 @@ namespace System.Text.Json.SourceGeneration return null; } - string clrName = memberInfo.Name; - string runtimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, options.PropertyNamingPolicy); - string propertyNameVarName = DeterminePropNameIdentifier(runtimePropertyName); + string effectiveJsonPropertyName = DetermineEffectiveJsonPropertyName(memberInfo.Name, jsonPropertyName, options); + string propertyNameFieldName = DeterminePropertyNameFieldName(effectiveJsonPropertyName); // Enqueue the property type for generation, unless the member is ignored. TypeRef propertyTypeRef = ignoreCondition != JsonIgnoreCondition.Always @@ -920,8 +1011,8 @@ namespace System.Text.Json.SourceGeneration IsPublic = isAccessible, IsVirtual = memberInfo.IsVirtual(), JsonPropertyName = jsonPropertyName, - RuntimePropertyName = runtimePropertyName, - PropertyNameVarName = propertyNameVarName, + EffectiveJsonPropertyName = effectiveJsonPropertyName, + PropertyNameFieldName = propertyNameFieldName, IsReadOnly = isReadOnly, IsRequired = isRequired, HasJsonRequiredAttribute = hasJsonRequiredAttribute, @@ -976,7 +1067,7 @@ namespace System.Text.Json.SourceGeneration if (converterType is null && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType)) { - converterType = GetConverterTypeFromAttribute(contextType, memberInfo, attributeData); + converterType = GetConverterTypeFromJsonConverterAttribute(contextType, memberInfo, attributeData); } else if (attributeType.ContainingAssembly.Name == SystemTextJsonNamespace) { @@ -1244,14 +1335,18 @@ namespace System.Text.Json.SourceGeneration return propertyInitializers; } - private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, ISymbol declaringSymbol, AttributeData attributeData) + private TypeRef? GetConverterTypeFromJsonConverterAttribute(INamedTypeSymbol contextType, ISymbol declaringSymbol, AttributeData attributeData) { Debug.Assert(_knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeData.AttributeClass)); - var converterType = (INamedTypeSymbol?)attributeData.ConstructorArguments[0].Value; + var converterType = (ITypeSymbol?)attributeData.ConstructorArguments[0].Value; + return GetConverterTypeFromAttribute(contextType, converterType, declaringSymbol, attributeData); + } - if (converterType == null || - !_knownSymbols.JsonConverterType.IsAssignableFrom(converterType) || - !converterType.Constructors.Any(c => c.Parameters.Length == 0 && IsSymbolAccessibleWithin(c, within: contextType))) + private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, ITypeSymbol? converterType, ISymbol declaringSymbol, AttributeData attributeData) + { + if (converterType is not INamedTypeSymbol namedConverterType || + !_knownSymbols.JsonConverterType.IsAssignableFrom(namedConverterType) || + !namedConverterType.Constructors.Any(c => c.Parameters.Length == 0 && IsSymbolAccessibleWithin(c, within: contextType))) { ReportDiagnostic(DiagnosticDescriptors.JsonConverterAttributeInvalidType, attributeData.GetDiagnosticLocation(), converterType?.ToDisplayString() ?? "null", declaringSymbol.ToDisplayString()); return null; @@ -1265,30 +1360,24 @@ namespace System.Text.Json.SourceGeneration return new TypeRef(converterType); } - private static string DetermineRuntimePropName(string clrPropName, string? jsonPropName, JsonKnownNamingPolicy namingPolicy) + private static string DetermineEffectiveJsonPropertyName(string propertyName, string? jsonPropertyName, SourceGenerationOptionsSpec? options) { - string runtimePropName; - - if (jsonPropName != null) + if (jsonPropertyName != null) { - runtimePropName = jsonPropName; + return jsonPropertyName; } - else - { - JsonNamingPolicy? instance = namingPolicy switch - { - JsonKnownNamingPolicy.CamelCase => JsonNamingPolicy.CamelCase, - JsonKnownNamingPolicy.SnakeCaseLower => JsonNamingPolicy.SnakeCaseLower, - JsonKnownNamingPolicy.SnakeCaseUpper => JsonNamingPolicy.SnakeCaseUpper, - JsonKnownNamingPolicy.KebabCaseLower => JsonNamingPolicy.KebabCaseLower, - JsonKnownNamingPolicy.KebabCaseUpper => JsonNamingPolicy.KebabCaseUpper, - _ => null, - }; - runtimePropName = instance?.ConvertName(clrPropName) ?? clrPropName; - } + JsonNamingPolicy? instance = options?.GetEffectivePropertyNamingPolicy() switch + { + JsonKnownNamingPolicy.CamelCase => JsonNamingPolicy.CamelCase, + JsonKnownNamingPolicy.SnakeCaseLower => JsonNamingPolicy.SnakeCaseLower, + JsonKnownNamingPolicy.SnakeCaseUpper => JsonNamingPolicy.SnakeCaseUpper, + JsonKnownNamingPolicy.KebabCaseLower => JsonNamingPolicy.KebabCaseLower, + JsonKnownNamingPolicy.KebabCaseUpper => JsonNamingPolicy.KebabCaseUpper, + _ => null, + }; - return runtimePropName; + return instance?.ConvertName(propertyName) ?? propertyName; } private static string? DetermineImmutableCollectionFactoryMethod(string? immutableCollectionFactoryTypeFullName) @@ -1296,7 +1385,7 @@ namespace System.Text.Json.SourceGeneration return immutableCollectionFactoryTypeFullName is not null ? $"global::{immutableCollectionFactoryTypeFullName}.CreateRange" : null; } - private static string DeterminePropNameIdentifier(string runtimePropName) + private static string DeterminePropertyNameFieldName(string effectiveJsonPropertyName) { const string PropName = "PropName_"; @@ -1304,16 +1393,16 @@ namespace System.Text.Json.SourceGeneration // the rare case there is a C# property in a hex format. const string EncodedPropName = "EncodedPropName_"; - if (SyntaxFacts.IsValidIdentifier(runtimePropName)) + if (SyntaxFacts.IsValidIdentifier(effectiveJsonPropertyName)) { - return PropName + runtimePropName; + return PropName + effectiveJsonPropertyName; } // Encode the string to a byte[] and then convert to hexadecimal. // To make the generated code more readable, we could use a different strategy in the future // such as including the full class name + the CLR property name when there are duplicates, // but that will create unnecessary JsonEncodedText properties. - byte[] utf8Json = Encoding.UTF8.GetBytes(runtimePropName); + byte[] utf8Json = Encoding.UTF8.GetBytes(effectiveJsonPropertyName); StringBuilder sb = new StringBuilder( EncodedPropName, diff --git a/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs index d7e2745..598d78b 100644 --- a/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs @@ -35,16 +35,6 @@ namespace System.Text.Json.SourceGeneration public required ImmutableEquatableArray ContextClassDeclarations { get; init; } - public required JsonIgnoreCondition DefaultIgnoreCondition { get; init; } - - public required bool IgnoreReadOnlyFields { get; init; } - - public required bool IgnoreReadOnlyProperties { get; init; } - - public required bool IncludeFields { get; init; } - - public required JsonKnownNamingPolicy PropertyNamingPolicy { get; init; } - - public required bool WriteIndented { get; init; } + public required SourceGenerationOptionsSpec? GeneratedOptionsSpec { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs index 054e1ae..577e921 100644 --- a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs @@ -57,9 +57,12 @@ namespace System.Text.Json.SourceGeneration /// specified ahead-of-time via . /// Only used in fast-path serialization logic. /// - public required string RuntimePropertyName { get; init; } + public required string EffectiveJsonPropertyName { get; init; } - public required string PropertyNameVarName { get; init; } + /// + /// The field identifier used for storing JsonEncodedText for use by the fast-path serializer. + /// + public required string PropertyNameFieldName { get; init; } /// /// Whether the property has a set method. @@ -156,7 +159,7 @@ namespace System.Text.Json.SourceGeneration } // Discard fields when JsonInclude or IncludeFields aren't enabled. - if (!IsProperty && !HasJsonInclude && !contextSpec.IncludeFields) + if (!IsProperty && !HasJsonInclude && contextSpec.GeneratedOptionsSpec?.IncludeFields != true) { return false; } @@ -166,12 +169,12 @@ namespace System.Text.Json.SourceGeneration { if (IsProperty) { - if (contextSpec.IgnoreReadOnlyProperties) + if (contextSpec.GeneratedOptionsSpec?.IgnoreReadOnlyProperties == true) { return false; } } - else if (contextSpec.IgnoreReadOnlyFields) + else if (contextSpec.GeneratedOptionsSpec?.IgnoreReadOnlyFields == true) { return false; } diff --git a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs new file mode 100644 index 0000000..e1ddf2e --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs @@ -0,0 +1,55 @@ +// 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; + +namespace System.Text.Json.SourceGeneration +{ + /// + /// Models compile-time configuration of . + /// Properties are made nullable to model the presence or absence of a given configuration. + /// + public sealed record SourceGenerationOptionsSpec + { + public required JsonSourceGenerationMode? GenerationMode { get; init; } + + public required JsonSerializerDefaults? Defaults { get; init; } + + public required bool? AllowTrailingCommas { get; init; } + + public required ImmutableEquatableArray? Converters { get; init; } + + public required int? DefaultBufferSize { get; init; } + + public required JsonIgnoreCondition? DefaultIgnoreCondition { get; init; } + + public required JsonKnownNamingPolicy? DictionaryKeyPolicy { get; init; } + + public required bool? IgnoreReadOnlyFields { get; init; } + + public required bool? IgnoreReadOnlyProperties { get; init; } + + public required bool? IncludeFields { get; init; } + + public required int? MaxDepth { get; init; } + + public required JsonNumberHandling? NumberHandling { get; init; } + + public required JsonObjectCreationHandling? PreferredObjectCreationHandling { get; init; } + + public required bool? PropertyNameCaseInsensitive { get; init; } + + public required JsonKnownNamingPolicy? PropertyNamingPolicy { get; init; } + + public required JsonCommentHandling? ReadCommentHandling { get; init; } + + public required JsonUnknownTypeHandling? UnknownTypeHandling { get; init; } + + public required JsonUnmappedMemberHandling? UnmappedMemberHandling { get; init; } + + public required bool? WriteIndented { get; init; } + + public JsonKnownNamingPolicy? GetEffectivePropertyNamingPolicy() + => PropertyNamingPolicy ?? (Defaults is JsonSerializerDefaults.Web ? JsonKnownNamingPolicy.CamelCase : null); + } +} diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets index 06fccb0..e14905d 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets @@ -31,6 +31,7 @@ + @@ -41,10 +42,12 @@ + + @@ -67,6 +70,7 @@ + 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 15300ab..46feae5 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -1045,12 +1045,24 @@ namespace System.Text.Json.Serialization public sealed partial class JsonSourceGenerationOptionsAttribute : System.Text.Json.Serialization.JsonAttribute { public JsonSourceGenerationOptionsAttribute() { } + public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefaults defaults) { } + public bool AllowTrailingCommas { get { throw null; } set { } } + public System.Type[]? Converters { get { throw null; } set { } } + public int DefaultBufferSize { get { throw null; } set { } } public System.Text.Json.Serialization.JsonIgnoreCondition DefaultIgnoreCondition { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonKnownNamingPolicy DictionaryKeyPolicy { get { throw null; } set { } } public System.Text.Json.Serialization.JsonSourceGenerationMode GenerationMode { get { throw null; } set { } } public bool IgnoreReadOnlyFields { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool IncludeFields { get { throw null; } set { } } + public int MaxDepth { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonObjectCreationHandling PreferredObjectCreationHandling { get { throw null; } set { } } + public bool PropertyNameCaseInsensitive { get { throw null; } set { } } public System.Text.Json.Serialization.JsonKnownNamingPolicy PropertyNamingPolicy { get { throw null; } set { } } + public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Applications should use the generic JsonStringEnumConverter instead.")] 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 49b9be5..8b5b4cf 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -28,6 +28,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -39,10 +40,12 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + @@ -61,7 +64,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - @@ -249,12 +251,10 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - - 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 bb77e52..b3564a7 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 @@ -55,17 +55,11 @@ namespace System.Text.Json.Serialization JsonSerializerOptions? generatedSerializerOptions = GeneratedSerializerOptions; - if (ReferenceEquals(options, generatedSerializerOptions)) - { - // Fast path for the 99% case - return true; - } - return generatedSerializerOptions is not null && // Guard against unsupported features options.Converters.Count == 0 && - options.Encoder == null && + options.Encoder is null && // Disallow custom number handling we'd need to honor when writing. // AllowReadingFromString and Strict are fine since there's no action to take when writing. !JsonHelpers.RequiresSpecialNumberHandlingOnWrite(options.NumberHandling) && @@ -80,8 +74,7 @@ namespace System.Text.Json.Serialization options.IgnoreReadOnlyProperties == generatedSerializerOptions.IgnoreReadOnlyProperties && options.IncludeFields == generatedSerializerOptions.IncludeFields && options.PropertyNamingPolicy == generatedSerializerOptions.PropertyNamingPolicy && - options.DictionaryKeyPolicy == generatedSerializerOptions.DictionaryKeyPolicy && - options.WriteIndented == generatedSerializerOptions.WriteIndented; + options.DictionaryKeyPolicy is null; } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 7a5f2f8..72b3e42 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -139,6 +139,8 @@ namespace System.Text.Json /// The to reason about. public JsonSerializerOptions(JsonSerializerDefaults defaults) : this() { + // Should be kept in sync with equivalent overload in JsonSourceGenerationOptionsAttribute + if (defaults == JsonSerializerDefaults.Web) { _propertyNameCaseInsensitive = true; diff --git a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs index 2428e97..23b66ab 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Text.RegularExpressions; using System.Threading.Tasks; using Xunit; @@ -144,6 +147,39 @@ namespace System.Text.Json } } + public static void AssertOptionsEqual(JsonSerializerOptions expected, JsonSerializerOptions actual) + { + foreach (PropertyInfo property in typeof(JsonSerializerOptions).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + Type propertyType = property.PropertyType; + + if (property.Name == nameof(JsonSerializerOptions.IsReadOnly)) + { + continue; // readonly-ness is not a structural property of JsonSerializerOptions. + } + else if (propertyType == typeof(IList)) + { + var expectedConverters = (IList)property.GetValue(expected); + var actualConverters = (IList)property.GetValue(actual); + Assert.Equal(expectedConverters.Count, actualConverters.Count); + for (int i = 0; i < actualConverters.Count; i++) + { + Assert.IsType(expectedConverters[i].GetType(), actualConverters[i]); + } + } + else if (propertyType == typeof(IList)) + { + var list1 = (IList)property.GetValue(expected); + var list2 = (IList)property.GetValue(actual); + Assert.Equal(list1, list2); + } + else + { + Assert.Equal(property.GetValue(expected), property.GetValue(actual)); + } + } + } + /// /// Linq Cartesian product /// diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs new file mode 100644 index 0000000..005c7c5 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public static partial class JsonSourceGenerationOptionsTests + { + [Fact] + public static void ContextWithGeneralSerializerDefaults_GeneratesExpectedOptions() + { + JsonSerializerOptions expected = new(JsonSerializerDefaults.General) { TypeInfoResolver = ContextWithGeneralSerializerDefaults.Default }; + JsonSerializerOptions options = ContextWithGeneralSerializerDefaults.Default.Options; + + JsonTestHelper.AssertOptionsEqual(expected, options); + } + + [JsonSourceGenerationOptions(JsonSerializerDefaults.General)] + [JsonSerializable(typeof(PersonStruct))] + public partial class ContextWithGeneralSerializerDefaults : JsonSerializerContext + { } + + [Fact] + public static void ContextWithWebSerializerDefaults_GeneratesExpectedOptions() + { + JsonSerializerOptions expected = new(JsonSerializerDefaults.Web) { TypeInfoResolver = ContextWithWebSerializerDefaults.Default }; + JsonSerializerOptions options = ContextWithWebSerializerDefaults.Default.Options; + + JsonTestHelper.AssertOptionsEqual(expected, options); + } + + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)] + [JsonSerializable(typeof(PersonStruct))] + public partial class ContextWithWebSerializerDefaults : JsonSerializerContext + { } + + [Fact] + public static void ContextWithWebDefaultsAndOverriddenPropertyNamingPolicy_GeneratesExpectedOptions() + { + JsonSerializerOptions expected = new(JsonSerializerDefaults.Web) + { + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, + TypeInfoResolver = ContextWithWebDefaultsAndOverriddenPropertyNamingPolicy.Default, + }; + + JsonSerializerOptions options = ContextWithWebDefaultsAndOverriddenPropertyNamingPolicy.Default.Options; + + JsonTestHelper.AssertOptionsEqual(expected, options); + } + + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower)] + [JsonSerializable(typeof(PersonStruct))] + public partial class ContextWithWebDefaultsAndOverriddenPropertyNamingPolicy : JsonSerializerContext + { } + + [Fact] + public static void ContextWithAllOptionsSet_GeneratesExpectedOptions() + { + JsonSerializerOptions expected = new(JsonSerializerDefaults.Web) + { + AllowTrailingCommas = true, + Converters = { new JsonStringEnumConverter(), new JsonStringEnumConverter() }, + DefaultBufferSize = 128, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseUpper, + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + IncludeFields = true, + MaxDepth = 1024, + NumberHandling = JsonNumberHandling.WriteAsString, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper, + ReadCommentHandling = JsonCommentHandling.Skip, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + WriteIndented = true, + + TypeInfoResolver = ContextWithAllOptionsSet.Default, + }; + + JsonSerializerOptions options = ContextWithAllOptionsSet.Default.Options; + + JsonTestHelper.AssertOptionsEqual(expected, options); + } + + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + AllowTrailingCommas = true, + Converters = new[] { typeof(JsonStringEnumConverter), typeof(JsonStringEnumConverter) }, + DefaultBufferSize = 128, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + DictionaryKeyPolicy = JsonKnownNamingPolicy.SnakeCaseUpper, + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + IncludeFields = true, + MaxDepth = 1024, + NumberHandling = JsonNumberHandling.WriteAsString, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseUpper, + ReadCommentHandling = JsonCommentHandling.Skip, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + WriteIndented = true)] + [JsonSerializable(typeof(PersonStruct))] + public partial class ContextWithAllOptionsSet : JsonSerializerContext + { } + + [Fact] + public static void ContextWithInvalidSerializerDefaults_ThrowsArgumentOutOfRangeException() + { + TypeInitializationException ex = Assert.Throws(() => ContextWithInvalidSerializerDefaults.Default); + ArgumentOutOfRangeException inner = Assert.IsType(ex.InnerException); + Assert.Contains("defaults", inner.Message); + } + + [JsonSourceGenerationOptions((JsonSerializerDefaults)(-1))] + [JsonSerializable(typeof(PersonStruct))] + public partial class ContextWithInvalidSerializerDefaults : JsonSerializerContext + { } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index c401cbe..75373ca 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -113,6 +113,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 575cd0e..7c4621f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -860,7 +860,7 @@ namespace System.Text.Json.Serialization.Tests var newOptions = new JsonSerializerOptions(options); Assert.False(newOptions.IsReadOnly); - VerifyOptionsEqual(options, newOptions); + JsonTestHelper.AssertOptionsEqual(options, newOptions); // No exception is thrown on mutating the new options instance because it is "unlocked". newOptions.ReferenceHandler = ReferenceHandler.Preserve; @@ -902,7 +902,7 @@ namespace System.Text.Json.Serialization.Tests { JsonSerializerOptions options = GetFullyPopulatedOptionsInstance(); var newOptions = new JsonSerializerOptions(options); - VerifyOptionsEqual(options, newOptions); + JsonTestHelper.AssertOptionsEqual(options, newOptions); } [Fact] @@ -937,7 +937,7 @@ namespace System.Text.Json.Serialization.Tests { var options = new JsonSerializerOptions { TypeInfoResolver = JsonSerializerOptions.Default.TypeInfoResolver }; JsonSerializerOptions optionsSingleton = JsonSerializerOptions.Default; - VerifyOptionsEqual(options, optionsSingleton); + JsonTestHelper.AssertOptionsEqual(options, optionsSingleton); } [Fact] @@ -1176,7 +1176,7 @@ namespace System.Text.Json.Serialization.Tests var options = new JsonSerializerOptions(); var newOptions = new JsonSerializerOptions(JsonSerializerDefaults.General); Assert.False(newOptions.IsReadOnly); - VerifyOptionsEqual(options, newOptions); + JsonTestHelper.AssertOptionsEqual(options, newOptions); } [Fact] @@ -1262,45 +1262,12 @@ namespace System.Text.Json.Serialization.Tests return options; } - private static void VerifyOptionsEqual(JsonSerializerOptions options, JsonSerializerOptions newOptions) - { - foreach (PropertyInfo property in typeof(JsonSerializerOptions).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - Type propertyType = property.PropertyType; - - if (property.Name == nameof(JsonSerializerOptions.IsReadOnly)) - { - continue; // readonly-ness is not a structural property of JsonSerializerOptions. - } - else if (propertyType == typeof(IList)) - { - var list1 = (IList)property.GetValue(options); - var list2 = (IList)property.GetValue(newOptions); - Assert.Equal(list1, list2); - } - else if (propertyType == typeof(IList)) - { - var list1 = (IList)property.GetValue(options); - var list2 = (IList)property.GetValue(newOptions); - Assert.Equal(list1, list2); - } - else if (propertyType.IsValueType) - { - Assert.Equal(property.GetValue(options), property.GetValue(newOptions)); - } - else - { - Assert.Same(property.GetValue(options), property.GetValue(newOptions)); - } - } - } - [Fact] public static void CopyConstructor_IgnoreNullValuesCopied() { var options = new JsonSerializerOptions { IgnoreNullValues = true }; var newOptions = new JsonSerializerOptions(options); - VerifyOptionsEqual(options, newOptions); + JsonTestHelper.AssertOptionsEqual(options, newOptions); } [Fact]