Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ private sealed class Parser

public List<Diagnostic> Diagnostics { get; } = new();
private Location? _contextClassLocation;
private JsonNumberHandling _contextNumberHandling;

public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object?[]? messageArgs)
{
Expand Down Expand Up @@ -151,6 +152,8 @@ public Parser(KnownTypeSymbols knownSymbols)
return null;
}

_contextNumberHandling = options?.GetEffectiveNumberHandling() ?? JsonNumberHandling.Strict;

// Enqueue attribute data for spec generation
foreach (TypeToGenerate rootSerializableType in rootSerializableTypes)
{
Expand Down Expand Up @@ -184,6 +187,7 @@ public Parser(KnownTypeSymbols knownSymbols)
_generatedTypes.Clear();
_typesToGenerate.Clear();
_contextClassLocation = null;
_contextNumberHandling = default;
return contextGenSpec;
}

Expand Down Expand Up @@ -1788,11 +1792,13 @@ private void EmitUnionAmbiguityDiagnostics(INamedTypeSymbol unionType, List<ITyp
{
string unionTypeName = unionType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
Dictionary<JsonValueType, List<string>> valueTypeToTypes = new();
JsonNumberHandling? unionNumberHandling = GetNumberHandling(unionType);

foreach (ITypeSymbol caseType in caseTypes)
{
string caseTypeName = caseType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
JsonValueType valueTypes = GetSupportedJsonValueTypes(caseType);
JsonNumberHandling effectiveNumberHandling = unionNumberHandling ?? GetNumberHandling(caseType) ?? _contextNumberHandling;
JsonValueType valueTypes = GetSupportedJsonValueTypes(caseType, effectiveNumberHandling);

for (int flag = 1; flag <= (int)JsonValueType.Boolean; flag <<= 1)
{
Expand Down Expand Up @@ -1837,7 +1843,7 @@ private void EmitUnionAmbiguityDiagnostics(INamedTypeSymbol unionType, List<ITyp
//
// User-defined converters are conservatively classified as potentially representing
// every JSON value shape, matching the JsonConverter base implementation.
private JsonValueType GetSupportedJsonValueTypes(ITypeSymbol type)
private JsonValueType GetSupportedJsonValueTypes(ITypeSymbol type, JsonNumberHandling numberHandling)
{
if (HasCustomConverterAttribute(type))
{
Expand Down Expand Up @@ -1877,7 +1883,7 @@ private JsonValueType GetSupportedJsonValueTypes(ITypeSymbol type)
SymbolEqualityComparer.Default.Equals(type, _knownSymbols.Decimal64Type) ||
SymbolEqualityComparer.Default.Equals(type, _knownSymbols.Decimal128Type))
{
return HasAllowReadingFromString(type)
return (numberHandling & JsonNumberHandling.AllowReadingFromString) != 0
? JsonValueType.Number | JsonValueType.String
: JsonValueType.Number;
}
Expand Down Expand Up @@ -1992,26 +1998,25 @@ private bool HasCustomConverterAttribute(ITypeSymbol type)
return false;
}

private bool HasAllowReadingFromString(ITypeSymbol type)
private JsonNumberHandling? GetNumberHandling(ITypeSymbol type)
{
INamedTypeSymbol? numberHandlingAttr = _knownSymbols.JsonNumberHandlingAttributeType;
if (numberHandlingAttr is null)
{
return false;
return null;
}

foreach (AttributeData attr in type.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, numberHandlingAttr) &&
attr.ConstructorArguments.Length > 0 &&
attr.ConstructorArguments[0].Value is int handlingValue &&
((JsonNumberHandling)handlingValue & JsonNumberHandling.AllowReadingFromString) != 0)
attr.ConstructorArguments[0].Value is int handlingValue)
{
return true;
return (JsonNumberHandling)handlingValue;
}
}

return false;
return null;
}

private bool TryResolveCollectionType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ public sealed record SourceGenerationOptionsSpec

public required bool? InferClosedTypePolymorphism { get; init; }

public JsonNumberHandling GetEffectiveNumberHandling()
=> NumberHandling ?? (Defaults is JsonSerializerDefaults.Web ? JsonNumberHandling.AllowReadingFromString : JsonNumberHandling.Strict);

public JsonKnownNamingPolicy? GetEffectivePropertyNamingPolicy()
=> PropertyNamingPolicy ?? (Defaults is JsonSerializerDefaults.Web ? JsonKnownNamingPolicy.CamelCase : null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ private static JsonSchema MapJsonSchemaCore(
if (effectiveConverter.NullableElementConverter is { } elementConverter)
{
JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(elementConverter.Type!);
schema = MapJsonSchemaCore(ref state, elementTypeInfo, customConverter: elementConverter, cacheResult: false);
schema = MapJsonSchemaCore(ref state, elementTypeInfo, customConverter: elementConverter, customNumberHandling: customNumberHandling ?? typeInfo.NumberHandling, cacheResult: false);

if (elementConverter.IsIeeeFloatingPointConverter &&
(effectiveNumberHandling & JsonNumberHandling.AllowNamedFloatingPointLiterals) != 0)
Expand Down Expand Up @@ -371,7 +371,7 @@ private static JsonSchema MapJsonSchemaCore(
JsonTypeInfo caseTypeInfo = typeInfo.Options.GetTypeInfoInternal(caseInfo.CaseType);

state.PushSchemaNode(unionAnyOf.Count.ToString(CultureInfo.InvariantCulture));
JsonSchema caseSchema = MapJsonSchemaCore(ref state, caseTypeInfo, cacheResult: false);
JsonSchema caseSchema = MapJsonSchemaCore(ref state, caseTypeInfo, customNumberHandling: typeInfo.NumberHandling, cacheResult: false);
state.PopSchemaNode();

if (caseInfo.IsNullable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,

caseTypeInfo = options.GetTypeInfoInternal(caseType);
state.Current.JsonPropertyInfo = caseTypeInfo.PropertyInfoForTypeInfo;
state.Current.NumberHandling = typeInfo.NumberHandling ?? caseTypeInfo.PropertyInfoForTypeInfo.EffectiveNumberHandling;
}

JsonConverter caseConverter = caseTypeInfo.Converter;
Expand Down Expand Up @@ -185,6 +186,7 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, TUnion value, JsonSeria

JsonTypeInfo caseTypeInfo = options.GetTypeInfoInternal(caseType);
state.Current.JsonPropertyInfo = caseTypeInfo.PropertyInfoForTypeInfo;
state.Current.NumberHandling = typeInfo.NumberHandling ?? caseTypeInfo.PropertyInfoForTypeInfo.EffectiveNumberHandling;
return caseTypeInfo.Converter.TryWriteAsObject(writer, caseValue, options, ref state);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ public sealed class JsonTypeClassifierContext
/// Initializes a new instance of the <see cref="JsonTypeClassifierContext"/> class.
/// </summary>
/// <param name="kind">The type of classifier metadata being configured.</param>
/// <param name="declaringType">The type being configured for classification.</param>
/// <param name="declaringTypeInfo">The contract being configured for classification.</param>
/// <param name="unionCases">The union cases of the declaring type, or an empty list.</param>
/// <param name="derivedTypes">The derived types of the declaring type, or an empty list.</param>
/// <param name="typeDiscriminatorPropertyName">The JSON property name used for type discrimination, or <see langword="null"/>.</param>
internal JsonTypeClassifierContext(
JsonTypeClassifierKind kind,
Type declaringType,
JsonTypeInfo declaringTypeInfo,
IReadOnlyList<JsonUnionCaseInfo> unionCases,
IReadOnlyList<JsonDerivedType> derivedTypes,
string? typeDiscriminatorPropertyName)
{
Kind = kind;
DeclaringType = declaringType;
DeclaringTypeInfo = declaringTypeInfo;
UnionCases = unionCases;
DerivedTypes = derivedTypes;
TypeDiscriminatorPropertyName = typeDiscriminatorPropertyName;
Expand All @@ -58,7 +58,10 @@ internal JsonTypeClassifierContext(
/// For polymorphic types, this is the base class (e.g., <c>Animal</c>).
/// For union types, this is the union type (e.g., <c>IntOrString</c>).
/// </remarks>
public Type DeclaringType { get; }
public Type DeclaringType => DeclaringTypeInfo.Type;

// The contract may still be mutable when a modifier reads TypeClassifier.
internal JsonTypeInfo DeclaringTypeInfo { get; }

/// <summary>
/// Gets the union cases of <see cref="DeclaringType"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,18 @@ public override JsonTypeClassifier CreateJsonClassifier(
ThrowHelper.ThrowNotSupportedException_UnionTypeStructuralClassifierPreserveReferencesNotSupported(context.DeclaringType);
}

StructuralClassifier classifier = BuildStructuralClassifier(context.DeclaringType, context.UnionCases, options);
StructuralClassifier classifier = BuildStructuralClassifier(context.DeclaringTypeInfo, context.UnionCases, options);
return classifier.Classify;
}

private static StructuralClassifier BuildStructuralClassifier(
Type unionType,
JsonTypeInfo unionTypeInfo,
IReadOnlyList<JsonUnionCaseInfo> unionCases,
JsonSerializerOptions options)
{
Type unionType = unionTypeInfo.Type;
JsonNumberHandling? unionNumberHandling = unionTypeInfo.NumberHandling;
Comment thread
eiriktsarpalis marked this conversation as resolved.

// POCO object cases expose JsonPropertyInfo metadata through JsonTypeInfoKind.Object.
// A non-POCO JSON object case advertises the Object shape without such metadata.
Dictionary<JsonValueType, Type> shapeBasedCases = new();
Expand All @@ -98,6 +101,7 @@ private static StructuralClassifier BuildStructuralClassifier(
AddCase(
unionType,
unionCase.CaseType,
unionNumberHandling,
options,
shapeBasedCases,
pocoObjectCaseList,
Expand Down Expand Up @@ -163,12 +167,14 @@ static void AddPocoPropertyInfo<TKey>(
private static void AddCase(
Type unionType,
Type caseType,
JsonNumberHandling? unionNumberHandling,
JsonSerializerOptions options,
Dictionary<JsonValueType, Type> shapeBasedCases,
List<PocoObjectCase> pocoObjectCases,
ref int requiredPropertyCount)
{
JsonTypeInfo typeInfo = options.GetTypeInfo(caseType);
JsonNumberHandling? numberHandlingOverride = unionNumberHandling ?? typeInfo.NumberHandling;
if (typeInfo is { IsNullable: true, ElementTypeInfo: JsonTypeInfo elementTypeInfo })
{
typeInfo = elementTypeInfo;
Expand All @@ -188,7 +194,7 @@ private static void AddCase(
caseType);
}

JsonNumberHandling numberHandling = typeInfo.NumberHandling ?? options.NumberHandling;
JsonNumberHandling numberHandling = numberHandlingOverride ?? typeInfo.NumberHandling ?? options.NumberHandling;
JsonValueType valueTypes = typeInfo.Converter.GetSupportedJsonValueTypes(numberHandling);

bool isPocoObjectCase = typeInfo is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,7 @@ private void ConfigureTypeClassifier()
Debug.Assert(UnionCases.Count > 0);
ctx = new JsonTypeClassifierContext(
JsonTypeClassifierKind.Union,
Type,
this,
new List<JsonUnionCaseInfo>(UnionCases),
Array.Empty<JsonDerivedType>(),
typeDiscriminatorPropertyName: null);
Expand All @@ -1108,7 +1108,7 @@ private void ConfigureTypeClassifier()

ctx = new JsonTypeClassifierContext(
JsonTypeClassifierKind.PolymorphicType,
Type,
this,
Array.Empty<JsonUnionCaseInfo>(),
new List<JsonDerivedType>(polymorphismOptions.DerivedTypes),
polymorphismOptions.TypeDiscriminatorPropertyName);
Expand Down Expand Up @@ -1175,7 +1175,7 @@ private static void BuildUnionValueTypeMap(IList<JsonUnionCaseInfo> unionCases,
}

JsonNumberHandling effectiveNumberHandling =
caseTypeInfo.NumberHandling ?? options.NumberHandling;
target.NumberHandling ?? caseTypeInfo.NumberHandling ?? options.NumberHandling;
Comment thread
eiriktsarpalis marked this conversation as resolved.
Comment thread
eiriktsarpalis marked this conversation as resolved.
Comment thread
eiriktsarpalis marked this conversation as resolved.
Comment thread
eiriktsarpalis marked this conversation as resolved.
Comment thread
eiriktsarpalis marked this conversation as resolved.
Comment thread
eiriktsarpalis marked this conversation as resolved.
JsonValueType valueTypes = converter.GetSupportedJsonValueTypes(effectiveNumberHandling);

AddUnionValueTypes(valueTypes, caseType, map, ref ambiguousValueTypes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,13 @@ public class PocoWithCustomNaming
public string? StringProperty { get; set; }
}

[JsonNumberHandling(JsonNumberHandling.Strict)]
public union StrictIntOrStringUnion(int, string);

public union IntOrBoolUnion(int, bool);

public union NullableIntUnion(int?);

[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public class PocoWithCustomNumberHandling
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,84 @@ public void TestTypes_SerializedValueMatchesGeneratedSchema(ITestData testData)
AssertDocumentMatchesSchema(schema, instance);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void UnionNumberHandling_StrictTypeAttributeOverridesWebDefaults(bool asCollectionElement)
{
JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
{
TypeInfoResolver = Serializer.DefaultOptions.TypeInfoResolver,
};

Type type = asCollectionElement ? typeof(List<StrictIntOrStringUnion>) : typeof(StrictIntOrStringUnion);
JsonNode schema = Serializer.GetTypeInfo(type, options).GetJsonSchemaAsNode();
JsonNode unionSchema = asCollectionElement ? schema["items"]! : schema;

JsonTestHelper.AssertJsonEqual("""{"type":"integer"}""", unionSchema["anyOf"]![0]!.ToJsonString());
}

[Theory]
[MemberData(nameof(JsonTestHelper.GetUnionCaseNumberHandlingPrecedenceTestData), MemberType = typeof(JsonTestHelper))]
public void UnionNumberHandling_MetadataOverrides(
JsonNumberHandling globalHandling, JsonNumberHandling? unionHandling, JsonNumberHandling? caseHandling, JsonNumberHandling expectedHandling)
{
foreach ((Type unionType, Type numberType) in new[] { (typeof(IntOrBoolUnion), typeof(int)), (typeof(NullableIntUnion), typeof(int?)) })
{
JsonSerializerOptions options = Serializer.CreateOptions(
configure: options => options.NumberHandling = globalHandling,
modifier: typeInfo =>
{
if (typeInfo.Type == numberType)
{
typeInfo.NumberHandling = caseHandling;
}
});

JsonTypeInfo typeInfo = Serializer.GetTypeInfo(unionType, options, mutable: true);
typeInfo.NumberHandling = unionHandling;
JsonNode schema = typeInfo.GetJsonSchemaAsNode();
JsonNode numberSchema = numberType == typeof(int) ? schema["anyOf"]![0]! : schema;
JsonNode schemaType = numberSchema["type"]!;
IEnumerable<string?> actualTypes = schemaType is JsonArray types
? types.Select(type => (string?)type)
: [(string?)schemaType];

List<string> expectedTypes = ["integer"];
if ((expectedHandling & (JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)) != 0)
{
expectedTypes.Add("string");
}
if (numberType == typeof(int?))
{
expectedTypes.Add("null");
}

Assert.Equal(expectedTypes.OrderBy(type => type, StringComparer.Ordinal), actualTypes.OrderBy(type => type, StringComparer.Ordinal));
}
}

[Theory]
[InlineData(JsonNumberHandling.AllowReadingFromString, JsonNumberHandling.Strict)]
[InlineData(JsonNumberHandling.Strict, JsonNumberHandling.AllowReadingFromString)]
public void UnionNumberHandling_NullableCasePreservesElementOverride(JsonNumberHandling globalHandling, JsonNumberHandling elementHandling)
{
JsonSerializerOptions options = Serializer.CreateOptions(
configure: options => options.NumberHandling = globalHandling,
modifier: typeInfo =>
{
if (typeInfo.Type == typeof(int))
{
typeInfo.NumberHandling = elementHandling;
}
});

bool allowsStrings = (elementHandling & JsonNumberHandling.AllowReadingFromString) != 0;
JsonNode schema = Serializer.GetTypeInfo<NullableIntUnion>(options).GetJsonSchemaAsNode();
JsonArray types = Assert.IsType<JsonArray>(schema["type"]);
Assert.Equal(allowsStrings, types.Any(type => (string?)type == "string"));
}

[Theory]
[InlineData(typeof(string), "string")]
[InlineData(typeof(int[]), "array")]
Expand Down
Loading
Loading