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

Example code

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

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.