How to Return Enums as Strings in Minimal API JSON
By default, an enum in a minimal API's JSON is a number, and "type": 0 tells a reader nothing. One line in Program.cs makes every endpoint use the names, in both directions. It's worth knowing exactly what it accepts on the way in, too.
Prerequisites
- An ASP.NET Core minimal API. I used ASP.NET Core 10.
Step 1: Add the Converter
// Enums travel as their names, such as "Receipt", not as numbers.
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
This needs using System.Text.Json.Serialization;. ConfigureHttpJsonOptions sets the options minimal APIs use. It doesn't affect controllers, which have their own.
Step 2: See the Responses Change
Before:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/v1/products/9/stock/movements
{
"movement": {
"id": 1,
"productId": 9,
"type": 0,
"quantityChange": 10,
"reason": "First delivery",
"reference": "PO-1001",
"orderId": null,
"occurredAtUtc": "2026-11-05T05:11:52.204666Z"
},
"quantityOnHand": 10
}
After:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/v1/products/9/stock/movements
{
"movement": {
"id": 2,
"productId": 9,
"type": "Receipt",
"quantityChange": 5,
"reason": "Second delivery",
"reference": "PO-1003",
"orderId": null,
"occurredAtUtc": "2026-11-05T05:12:38.543732Z"
},
"quantityOnHand": 15
}

Step 3: See What It Accepts
For this request type:
public sealed record EnumBody(StockMovementType Type, OrderStatus? Status);
g.MapPost("/enum", (EnumBody body) => Results.Ok(body));
| Body | Result |
|---|---|
{"type":"Receipt","status":"Placed"} | Accepted |
{"type":"receipt"} | Accepted: the case doesn't matter |
{"type":1,"status":2} | Accepted: numbers still work |
{"type":"Refund"} | 400: not a member |
Here's the numbers row:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"type": "Adjustment",
"status": "Cancelled"
}
And the last one:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "00-48b2a0e4f443cf6adddb0732ed560e67-71a3cb1d84eaa9a3-00"
}
Step 4: Refuse Numbers
If you want names only, say so:
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false)));
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "00-e141a48533df0d1680fa2f16d0f7d5cc-bc51ae9f5a2934fe-00"
}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"type": "Receipt",
"status": null
}
Common Mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Expecting this to change the query string | JSON options don't apply there | Handle it separately |
| Renaming an enum member | Clients break, since the name is now the contract | Add a new one, or map it |
| Adding it per property | Easy to miss one | Register it once |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Still numbers | It's on AddControllers, or on another options object | Use ConfigureHttpJsonOptions |
400 with no explanation | A JSON value that isn't a member | The reason is in the log |
?status=99 is accepted | Query strings parse differently | The fix |
Conclusion
Add JsonStringEnumConverter once, in ConfigureHttpJsonOptions. Pass allowIntegerValues: false if you don't want numbers accepted.
Additional Resources
- JsonStringEnumConverter — the converter.
