Runtime type for subclasses

This recipe is useful when you need to revive fields where several subclasses can appear.

An example of this is JSON generated using Gson's TypeAdapterarrow-up-right. In fact, this example is based on Gson's RuntimeTypeAdapterFactoryarrow-up-right

We will use ajvMetadata to demonstrate how to get Modélico to generate complex JSON schemas. This is also an example of a schema with circular references.

class Geometer extends M.Base {
  static innerTypes () {
    return Object.freeze({
      name: string({minLength: 1}),
      // Shape has multiple subclasses
      favouriteShape: _(Shape)
    })
  }
}

The Shape in the inner types can be either a Diamond or a Circle. Let's look at their definitions:

const ShapeType = M.Enum.fromArray(['CIRCLE', 'DIAMOND'])

// We are going to use this metadata in several places, so by reusing it, we
// not only save unnecessary processing, but our generated JSON schema will
// also be more compact.
const greaterThanZero = number({
  exclusiveMinimum: 0
})

const reviver = (k, v) => {
  if (k !== '') {
    return v
  }

  switch (v.type) {
    case ShapeType.CIRCLE().toJSON():
      return new Circle(v)
    case ShapeType.DIAMOND().toJSON():
      return new Diamond(v)
    default:
      throw TypeError('Unsupported or missing shape type in the Shape reviver.')
  }
}

class Shape extends M.Base {
  toJSON () {
    const fields = M.fields(this)
    let type

    // the generated JSON must contain runtime type information for Modélico
    // to be able to revive it later
    switch (this[M.symbols.typeSymbol]()) {
      case Circle:
        type = ShapeType.CIRCLE()
        break
      case Diamond:
        type = ShapeType.DIAMOND()
        break
      default:
        throw TypeError('Unsupported Shape in the toJSON method.')
    }

    return Object.freeze(Object.assign({type}, fields))
  }

  static innerTypes () {
    return Object.freeze({
      // notice the self-reference here and how the subclasses are
      // going to extend Shape's inner types
      relatedShape: maybe(_(Shape))
    })
  }

  static metadata () {
    // We are going to use meta so that M.getSchema can give us a more
    // robust JSON schema. If you don't need that, you could return the
    // baseMetadata directly.
    const baseMetadata = Object.assign({}, base(Shape), {reviver})

    return meta(baseMetadata, {}, {}, () => ({
      anyOf: [
        Circle,
        Diamond
      ].map(x => M.getSchema(base(x), false))
    }))
  }
}

With that, we can revive Diamonds

as well as Circles:

Serialisation also works for Diamonds and Circles

respectively yield:

Now remember how we have been using ajvMetadata and even enhanced the Shape metadata to account for its subtypes. This is going to allow us to get a very detailed schema that would not be easy to write by hand.

Note: definitions are sequentially named to avoid collisions. In the example below, the definition numbers have gaps because some of them got reserved in case they'd be reused. Short sub-schemas (less than 2 keys and not arrays) are always inlined.

yields:

Last updated