I'm trying to get proper enum generation via OpenAPI in my ASP.NET Core 9 project so I can create a typescript schema using
npx openapi-typescript https://localhost:7088/openapi/v1.json -o ./apiSchema.ts --enum
Since the default OpenAPI implementation in .NET 9 does only generate enums as integers, I looked into schema transformers. However I did not fully find out how to use them nor found docs which helped me with the issue that it doubles up the schema.
var enumCache = new Dictionary<Type, OpenApiSchema>();
options.AddSchemaTransformer(async delegate(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken ct)
{
if (context?.JsonPropertyInfo?.PropertyType.IsEnum == true)
{
if (enumCache.TryGetValue(context.JsonPropertyInfo.PropertyType, out var enumSchema))
{
Console.WriteLine($"prop: {context?.JsonPropertyInfo?.Name}: enumSchema:{enumSchema} - from cache");
schema = enumSchema;
return;
}
var enumNames = Enum.GetNames(context.JsonPropertyInfo.PropertyType);
var enumValues = Enum.GetValues(context.JsonPropertyInfo.PropertyType);
var enumDesc = enumValues.Cast<object>().Select(
(value, index) =>
{
// get description attribute via reflection
var attr = context.JsonPropertyInfo.PropertyType.GetField(value.ToString())?
.GetCustomAttribute<DescriptionAttribute>();
return new
{
Value = (int) value,
Name = enumNames[index],
Description = attr?.Description
};
});
var openApiValueArray = new OpenApiArray();
var openApiNameArray = new OpenApiArray();
var openApiDescArray = new OpenApiArray();
foreach (var item in enumDesc)
{
openApiValueArray.Add(new OpenApiInteger(item.Value));
openApiNameArray.Add(new OpenApiString(item.Name));
openApiDescArray.Add(new OpenApiString(item.Description));
}
schema.Extensions.Add("x-enum-varnames", openApiNameArray); // /advanced#enum-extensions
schema.Extensions.Add("x-enum-descriptions", openApiDescArray);
schema.Extensions.Add("enum", openApiValueArray);
enumCache[context.JsonPropertyInfo.PropertyType] = schema;
return;
}
return;
});
This is an (partial) example where the types double up. This also affects all containing types and the endpoints then use the wrong "old" schema.
"WorldStatus": {
"type": "integer",
"x-enum-varnames": [
"Active",
"Maintenance",
"Full",
"Ended"
],
"x-enum-descriptions": [
null,
null,
null,
"game over"
],
"enum": [
0,
1,
2,
3
]
},
"WorldStatus2": {
"type": "integer"
}
Test enum:
public enum WorldStatus
{
Active,
Maintenance,
Full,
[Description("game over")]
Ended
}
So my question is on how to remove the default/original schema. I have the feeling there needs to be an easier way then also creating a OperationTransformer
which check each type and its inner types.
I'm trying to get proper enum generation via OpenAPI in my ASP.NET Core 9 project so I can create a typescript schema using
npx openapi-typescript https://localhost:7088/openapi/v1.json -o ./apiSchema.ts --enum
Since the default OpenAPI implementation in .NET 9 does only generate enums as integers, I looked into schema transformers. However I did not fully find out how to use them nor found docs which helped me with the issue that it doubles up the schema.
var enumCache = new Dictionary<Type, OpenApiSchema>();
options.AddSchemaTransformer(async delegate(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken ct)
{
if (context?.JsonPropertyInfo?.PropertyType.IsEnum == true)
{
if (enumCache.TryGetValue(context.JsonPropertyInfo.PropertyType, out var enumSchema))
{
Console.WriteLine($"prop: {context?.JsonPropertyInfo?.Name}: enumSchema:{enumSchema} - from cache");
schema = enumSchema;
return;
}
var enumNames = Enum.GetNames(context.JsonPropertyInfo.PropertyType);
var enumValues = Enum.GetValues(context.JsonPropertyInfo.PropertyType);
var enumDesc = enumValues.Cast<object>().Select(
(value, index) =>
{
// get description attribute via reflection
var attr = context.JsonPropertyInfo.PropertyType.GetField(value.ToString())?
.GetCustomAttribute<DescriptionAttribute>();
return new
{
Value = (int) value,
Name = enumNames[index],
Description = attr?.Description
};
});
var openApiValueArray = new OpenApiArray();
var openApiNameArray = new OpenApiArray();
var openApiDescArray = new OpenApiArray();
foreach (var item in enumDesc)
{
openApiValueArray.Add(new OpenApiInteger(item.Value));
openApiNameArray.Add(new OpenApiString(item.Name));
openApiDescArray.Add(new OpenApiString(item.Description));
}
schema.Extensions.Add("x-enum-varnames", openApiNameArray); // https://openapi-ts.dev/advanced#enum-extensions
schema.Extensions.Add("x-enum-descriptions", openApiDescArray);
schema.Extensions.Add("enum", openApiValueArray);
enumCache[context.JsonPropertyInfo.PropertyType] = schema;
return;
}
return;
});
This is an (partial) example where the types double up. This also affects all containing types and the endpoints then use the wrong "old" schema.
"WorldStatus": {
"type": "integer",
"x-enum-varnames": [
"Active",
"Maintenance",
"Full",
"Ended"
],
"x-enum-descriptions": [
null,
null,
null,
"game over"
],
"enum": [
0,
1,
2,
3
]
},
"WorldStatus2": {
"type": "integer"
}
Test enum:
public enum WorldStatus
{
Active,
Maintenance,
Full,
[Description("game over")]
Ended
}
So my question is on how to remove the default/original schema. I have the feeling there needs to be an easier way then also creating a OperationTransformer
which check each type and its inner types.
2 Answers
Reset to default 1I don't know about this feature, but I don't think this would work:
if (enumCache.TryGetValue(context.JsonPropertyInfo.PropertyType, out var enumSchema))
{
schema = enumSchema;
return;
}
You are assigning a value to the argument, which will only be passed back to the caller if the argument was passed by reference (ref or out). Which it doesn't seem to be.
So all this does is change the object the argument variable is referencing.
You should be instead modifying the schema
variable as you do in the rest of the method.
The issue was my caching as @juunas pointed out. I did not notice until his answer. It just started to work when I implemented it as Transformer class and left the caching unpropusfully out.
My final (working) solution:
options.AddSchemaTransformer<EnumSchemaTransformer>();
public class EnumSchemaTransformer : Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context?.JsonPropertyInfo?.PropertyType.IsEnum == true)
{
var enumType = context.JsonPropertyInfo.PropertyType;
var enumValues = Enum.GetValues(enumType).Cast<object>();
var enumDesc = enumValues.Select(value =>
{
var field = enumType.GetField(value.ToString());
return new
{
Value = (int)value,
field?.Name,
field?.GetCustomAttribute<DescriptionAttribute>()?.Description
};
});
var openApiValueArray = new OpenApiArray();
var openApiNameArray = new OpenApiArray();
var openApiDescArray = new OpenApiArray();
foreach (var item in enumDesc)
{
openApiValueArray.Add(new OpenApiInteger(item.Value));
openApiNameArray.Add(new OpenApiString(item.Name));
openApiDescArray.Add(new OpenApiString(item.Description));
}
schema.Extensions.Add("x-enum-varnames", openApiNameArray);
schema.Extensions.Add("x-enum-descriptions", openApiDescArray);
schema.Extensions.Add("enum", openApiValueArray);
return Task.CompletedTask;
}
return Task.CompletedTask;
}
}