JsonSchemaExporter.GetJsonSchemaAsNode cannot output array of fields for enum types with FlagsAttribute
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- csharp, json
- Domain
- backend-api-design
Research direction
Start with the GetJsonSchemaAsNode reproduction and read EnumConverter.cs at the linked source location, then inspect JsonSchemaExporterOptions and the TransformSchemaNode workaround. Done means a supported configuration can emit enum values for FlagsAttribute enums, including nullable cases, without breaking the existing non-flags behavior.
Written by the indexing model from the issue text.
Description
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
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/runtime
-
agentic-workflows untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
area-System.Reflection blocking-clean-ci-optional Known Build Error os-mac-os-x untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
area-CodeGen-coreclr untriaged
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
agentic-workflows untriaged
Difficulty 1/5 Under an hour Newbie friendliness 78/100
-
area-VM-meta-mono untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SubtitleEdit/subtitleedit#15108 · 1 comment ·
-
area/docs-content Bug pulumi/docs
Difficulty 1/5 1-3 hours Newbie friendliness 94/100
-
Create parent directories only after the containment check in InstallHelper.TryExtractToDirectory Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
PowerShell/PSResourceGet#2056 ·