Skip to content
Matthew Adams By Matthew Adams Co-Founder
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

Example code

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}"));
}

Matthew Adams

Co-Founder

Matthew Adams

Matthew was CTO of a venture-backed technology start-up in the UK & US for 10 years, and is now the co-founder of endjin, which provides technology strategy, experience and development services to its customers who are seeking to take advantage of Microsoft Azure and the Cloud.