Json Schema Patterns in .NET - Enumerations and pattern matching
In this series we are cataloging common patterns with JSON Schema and the .NET code generated by Corvus.JsonSchema.
It is especially useful for developers who want to move to a schema-first approach, but are unsure how that will map to their .NET code.
We are focusing on draft 2020-12 and the dotnet8.0 code generation, but very similar patterns apply for older versions of both .NET and draft 2019-09, draft 7, and even draft 6. We will highlight the key differences as we go.
If you have no experience of JSON Schema at all, I would recommend you read the getting started step-by-step documentation provided by the JSON Schema team.
Using enum
to create enumerations
In .NET, an enumeration is a labelled numeric type.
The default syntax assumes the numeric type is an Int32
, the first label gets the value 0
and they increment monotonically from there.
public enum MyEnum
{
Label1,
Label2,
Label3,
}
You can override the type and/or the value if you wish
public enum MyEnum : ushort
{
One = 1,
Four = 4,
Eight = 8,
}
However, JSON schema enum
values are not quite like that. You can provide any value as a valid enum value, and even mix and match the types of allowed values.
{
"enum": [1, 2, "three", {"value": 4}, ["five"]]
}
Typically, however, when defining an enum
we will either serialize as a numeric (integer) value, or as the string representation of the value.
Most commonly, we use the string value for maximum readability, and enum
works well for this case. Especially when the names of the values are essentially self documenting.
In the next article, we will look at a better approach for numeric enumerations, or where more documentation is needed.
File: currency-value.json
{
"description": "A value in a specific currency",
"required": ["currency", "value"],
"properties": {
"currency": {"$ref": "./currencies.json"},
"value": {"$ref": "#/$defs/monetaryValue"}
},
"$defs": {
"monetaryValue": {
"type": "number",
"format": "decimal"
}
}
}
File: currencies.json
{
"title": "A list of currencies",
"enum": [
"gbp",
"usd",
"eur"
]
}
In this case, we are saying that a currency-value
has two required
properties.
The first is the value
itself (represented as a decimal
number
).
The other is the currency
, which is one of a choice from the currencies.json
schema. The choice is defined by the values in the enum
, and those values are all strings.
Generate the code:
generatejsonschematypes --outputPath Model --rootNamespace JsonSchemaSample.Api currency-value.json
Generating: CurrencyValue
Generating: Currencies
(Notice that, as with the last time we defined a monetary value like this, there is no MonetaryValue
type generated because the schema reduces to one of the built-in types.)
using JsonSchemaSample.Api;
// Create currency values, using the permitted values in the enum
CurrencyValue valueInGbp =
CurrencyValue.Create(Currencies.EnumValues.Gbp, 123.45m);
CurrencyValue valueInEur =
CurrencyValue.Create(Currencies.EnumValues.Eur, 1234.56m);
// Get an enum value as a UTF8 byte array
ReadOnlySpan<byte> utf8Value = Currencies.EnumValues.GbpUtf8;
// Pass to a processing function. No polymorphism required.
Console.WriteLine(
ConvertToUsd(valueInGbp));
Console.WriteLine(
ConvertToUsd(valueInEur));
CurrencyValue ConvertToUsd(in CurrencyValue currencyValue)
{
// Use the match function on the enum
return currencyValue.Currency.Match(
// Pass in an (optional) state value. All pattern matching functions allow a state to be
// passed in addition to the value.
currencyValue,
// These match the currency type and return a new value in USD
// I like to use the optional parameter names to help with readability
matchGbp: cv => CurrencyValue.Create(Currencies.EnumValues.Usd, cv.Value / 1.25m),
matchUsd: cv => cv,
matchEur: cv => CurrencyValue.Create(Currencies.EnumValues.Usd, cv.Value / 1.08m),
defaultMatch: cv => throw new InvalidOperationException($"Unknown currency type: {cv.Currency}"));
}