JsonSchemaExporter.GetJsonSchemaAsNode cannot output array of fields for enum types with FlagsAttribute
#107,508 建立於 2024年9月8日
倉庫指標
- Star
- (17,886 star)
- PR 合併指標
- (平均合併 12天 11小時) (30 天內合併 661 個 PR)
描述
Description
According to the JSON Schema specification, enumerations are expected to select a single (possibly null) element from a list of exclusive elements, like System.DayOfWeek, and are not intended to be treated as bit fields like System.IO.FileAccess.
https://json-schema.org/draft/2020-12/json-schema-validation#name-enum
The current implementation is as follows:
- By default, enumerations are output as integer type
- If you register JsonStringEnumConverter to JsonSerializerOptions.Converters:
- For enum types without FlagsAttribute, the list of fields is output with the "enum" key. ("type" is not output)
- For enum types with FlagsAttribute, it is output as string type.
As the comments in the source code say, it is understandable that some precision is sacrificed for simplicity.
Treating an enumeration as a bit field is an implementation issue. It may be preferable to provide a list of possible values.
If you need to pass multiple values, you can define a separate array for example. Also, there are some combinations that are not allowed in bit fields, but these are not something that should be defined in a JSON Schema.
Therefore, I think it should be possible to output the list of fields with the "enum" key even for enumerations with FlagsAttribute by configuration.
Related issue: #107501
Reproduction Steps
Uses System.Text.Json 9.0.0-preview.7.24405.7. Run the following code:
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
Converters = { new JsonStringEnumConverter() }, // If not defined, it will be output as integer type.
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
RespectNullableAnnotations = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
WriteIndented = true
};
var exporterOptions = new JsonSchemaExporterOptions
{
TreatNullObliviousAsNonNullable = true,
};
var schema = serializerOptions.GetJsonSchemaAsNode(typeof(TestModel), exporterOptions);
Console.WriteLine(schema.ToJsonString(serializerOptions));
public class TestModel
{
public required DayOfWeek? EnumRequiredAllowNull { get; set; }
public required DayOfWeek EnumRequiredNotNull { get; set; }
public required FileAccess? EnumWithFlagsRequiredAllowNull { get; set; }
public required FileAccess EnumWithFlagsRequiredNotNull { get; set; }
}
Expected behavior
This does not have to be the default behavior, but the configuration should support it.
{
"type": "object",
"properties": {
"enum_required_allow_null": {
"enum": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
null
]
},
"enum_required_not_null": {
"enum": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
]
},
"enum_with_flags_required_allow_null": {
"enum": [
"Read",
"Write",
"ReadWrite",
null
]
},
"enum_with_flags_required_not_null": {
"enum": [
"Read",
"Write",
"ReadWrite"
]
}
},
"required": [
"enum_required_allow_null",
"enum_required_not_null",
"enum_with_flags_required_allow_null",
"enum_with_flags_required_not_null"
]
}
Actual behavior
- If you register JsonStringEnumConverter to JsonSerializerOptions.Converters:
- For enum types without FlagsAttribute, the list of fields is output with the "enum" key. ("type" is not output)
- For enum types with FlagsAttribute, it is output as string type.
{
"type": "object",
"properties": {
"enum_required_allow_null": {
"enum": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
null
]
},
"enum_required_not_null": {
"enum": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
]
},
"enum_with_flags_required_allow_null": {
"type": [
"string",
"null"
]
},
"enum_with_flags_required_not_null": {
"type": "string"
}
},
"required": [
"enum_required_allow_null",
"enum_required_not_null",
"enum_with_flags_required_allow_null",
"enum_with_flags_required_not_null"
]
}
Regression?
No response
Known Workarounds
Uses JsonSchemaExporterOptions.TransformSchemaNode. I generated the "Expected behavior" schema with the code below.
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
Converters = { new JsonStringEnumConverter() },
WriteIndented = true,
NumberHandling = JsonNumberHandling.Strict,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
RespectNullableAnnotations = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
var exporterOptions = new JsonSchemaExporterOptions
{
TreatNullObliviousAsNonNullable = true,
TransformSchemaNode = OnTransformSchemaNode
};
var schema = serializerOptions.GetJsonSchemaAsNode(typeof(TestModel), exporterOptions);
Console.WriteLine(schema.ToJsonString(serializerOptions));
JsonNode OnTransformSchemaNode(JsonSchemaExporterContext context, JsonNode node)
{
if(context.TypeInfo.Kind != JsonTypeInfoKind.Object)
{
return node;
}
var current = node.AsObject();
if(!current.TryGetPropertyValue("properties", out var properties))
{
return current;
}
if(properties is not JsonObject propertiesObject)
{
return current;
}
// Searches within "properties" and if the property type is enum, removes "type" and outputs "enum".
for(int i = 0; i < propertiesObject.Count; i++)
{
if(propertiesObject[i] is not JsonObject property)
{
continue;
}
if(!IsEnumType(context.TypeInfo, property.GetPropertyName(), out var enumType, out var isNullable))
{
continue;
}
TryUpdateEnum(property, enumType, isNullable);
}
return current;
}
void TryUpdateEnum(JsonObject property, Type enumType, bool isNullable)
{
if(property.TryGetPropertyValue("type", out _))
{
property.Remove("type");
}
if(!property.TryGetPropertyValue("enum", out _))
{
var values = new JsonArray();
foreach(var value in Enum.GetNames(enumType))
{
values.Add(value);
}
if(isNullable)
{
values.Add(null);
}
property.Add("enum", values);
}
}
bool IsEnumType(JsonTypeInfo typeInfo, string propertyName, [NotNullWhen(true)] out Type? type, out bool isNullable)
{
type = null;
isNullable = false;
// Determines if a property is an enum type, including case that it is nullable type.
var t = typeInfo.Properties.FirstOrDefault(r => r.Name == propertyName);
if(t == null)
{
return false;
}
var underlyingType = Nullable.GetUnderlyingType(t.PropertyType);
type = (t is { PropertyType.IsEnum: true }) ? t.PropertyType :
(underlyingType is { IsEnum: true }) ? underlyingType : null;
isNullable = underlyingType is not null;
return type != null;
}
public class TestModel
{
public required DayOfWeek? EnumRequiredAllowNull { get; set; }
public required DayOfWeek EnumRequiredNotNull { get; set; }
public required FileAccess? EnumWithFlagsRequiredAllowNull { get; set; }
public required FileAccess EnumWithFlagsRequiredNotNull { get; set; }
}
Configuration
No response
Other information
No response