Json Schema Patterns in .NET - Numeric 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 oneOf
to create documented, numeric enumerations
In the previous article we saw how to create string-based enumerations using the enum
type, dispatching them with pattern matching.
However, what about the other case we looked at - a fundamentally numeric enumeration? How do we best represent that?
public enum MyEnum
{
Foo = 1,
Bar = 4,
Baz = 8,
}
At first glance this might seem simple:
{
"enum": [1, 4, 8]
}
This is perfectly valid JSON schema, and will constrain our values to the numbers 1
, 4
or 8
.
But notice that we have lost the Foo
, Bar
, Baz
context from the original enum, and there is no way to document the values in the enumeration.
A common approach to the this problem is to use oneOf
and const
instead.
File: numeric-options.json
{
"oneOf": [
{ "$ref": "#/$defs/Foo" },
{ "$ref": "#/$defs/Bar" },
{ "$ref": "#/$defs/Baz" }
],
"$defs": {
"Foo": {
"title": "The foo item.",
"description": "Defines what the foo value means.",
"const": 1
},
"Bar": {
"title": "The bar item.",
"description": "Bar probably means something else.",
"const": 4
},
"Baz": {
"title": "The baz item.",
"description": "The less said about Baz the better.",
"const": 8
}
}
}
You may wish to use the same approach for string
enumerations, in order to benefit from the ability to document the values.
Generate the code:
generatejsonschematypes --outputPath Model --rootNamespace JsonSchemaSample.Api numeric-options.json
Generating: NumericOptions
Generating: Foo
Generating: Bar
Generating: Baz
using Corvus.Json;
using JsonSchemaSample.Api;
// Gets a constant instance of the enum value with implicit conversion to the enumerated type
NumericOptions options = NumericOptions.Foo.ConstInstance;
// Because our numeric values are not explicitly declared to be "type": "number",
// there are no implicit conversions from arbitrary numeric values.
// This helps you avoid accidentally creating invalid values
// NumericOptions.Foo invalidOption = 19; // This does not compile
// However, you can *explicitly* convert them if you are getting a numeric value
// from elsewhere in the system (which may create an invalid value)
NumericOptions invalidOption = (NumericOptions)19;
if (!invalidOption.IsValid())
{
Console.WriteLine("The invalid option is not valid!");
}
// The value itself is numeric
Console.WriteLine(options);
Console.WriteLine(
// Dispatch the value to a function in the usual way
ProcessOptions(options));
try
{
// The matcher handles the invalid case
ProcessOptions(invalidOption);
}
catch(InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
string ProcessOptions(NumericOptions options)
{
// You could pass some state.
// Here we are just using the options value.
return options.Match(
(in NumericOptions.Foo o) => $"It was a foo: {o}",
(in NumericOptions.Bar o) => $"It was a bar: {o}",
(in NumericOptions.Baz o) => $"It was a baz: {o}",
(in NumericOptions o) => throw new InvalidOperationException($"Unknown numeric option: {o}"));
}