Table of Contents

Advanced usage

Unions

In the previous examples, we have seen how to create object properties of a single type. However, in many real-world applications, data structure fields can be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type T or null (or a "union" between type T and null).

JSON Schema allows union types using the oneOf keyword. For example:

{
  "title": "MyPet",
  "type": "object",
  "properties": {
    "FooProperty": {
      "oneOf": [
        { "type": "string" },
        { "type": "number" }
      ]
    }
  }
}

Running Bonsai.Sgen on this schema generates the following type signature for FooProperty:

public object FooProperty

While oneOf is supported, statically typed languages like C# require the exact type at compile time. Thus, the property is "up-cast" to object, and you must down-cast it to the correct type at runtime.

Tagged-Unions

Union types can be made type-aware by using tagged unions (or discriminated unions). The syntax for tagged unions is not part of the JSON Schema specification, however it is supported by the OpenAPI standard, which is a superset of JSON Schema. The key idea behind tagged unions is to add a discriminator field to the schema that specifies the property that will be used to determine the type of the object at runtime.

For example, a Pet object that can be either a Dog or a Cat can be represented as follows:

person-and-discriminated-pets.json

"Pet": {
  "discriminator": {
    "mapping": {
      "cat": "#/$defs/Cat",
      "dog": "#/$defs/Dog"
    },
    "propertyName": "pet_type"
  },
  "oneOf": [
    { "$ref": "#/$defs/Dog" },
    { "$ref": "#/$defs/Cat" }
  ]
}

Given this schema, Bonsai.Sgen will generate a root type Pet that will be specialised by the Dog and Cat types (since in the worst case scenario, the discriminated property must be shared). The Pet type will have a pet_type property that will be used to downcast to the proper type at runtime. At this point we can open our example in Bonsai and see how the Pet type is represented in the workflow.

As you can see below, we still get a Pet type. Better than object, but still not a Dog or Cat type. Fortunately, Bonsai.Sgen will generate an operator that can be used to filter and downcast the Pet objects to the correct type at runtime. These are called Match<T> operators. MatchPet can be used to select the desired target type which will allow us access to the properties of the Dog or Cat subtypes. Conversely, we can also upcast a Dog or Cat to a Pet by leaving the MatchPet operator's Type property empty.

Discriminated Unions

Important

It is strongly recommended to use references with the oneOf syntax. Not only does this decision make your JSON Schema significantly smaller, it will also help Bonsai.Sgen generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, Bonsai.Sgen will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy.

Extending generated code with partial classes

Generated classes are marked as partial, allowing you to extend them without modifying the generated code directly. This can be done by placing the new .cs file in the Extensions folder of your project.

For example, to add an operator for summing Cat objects:

namespace PersonAndDiscriminatedPets
{
    partial class Cat
    {
        public static Cat operator +(Cat c1, Cat c2)
        {
            return new Cat
            {
                CanMeow = c1.CanMeow || c2.CanMeow,
                Age = c1.Age + c2.Age
            };
        }
    }
}

In Bonsai, use the Add operator to sum Cat objects:

Discriminated Unions

Supported tags

  • x-abstract: Marks a class as abstract, preventing it from being generated as an operator in Bonsai.