This algebra provides vocabulary to define JSON schemas of data types.
This module is dependency-free, it can be used independently of endpoints to define JSON schemas and interpret them as actual encoder, decoders or documentation.
The algebra introduces the concept of
JsonSchema[A]: a JSON schema for a type
The trait provides some predefined JSON schemas (for
and ways to combine them together to build more complex schemas.
For instance, given the following
Rectangle data type:
We can represent instances of
Rectangle in JSON with a JSON object having properties corresponding
to the case class fields. A JSON schema for such objects would be defined as follows:
field constructor defines a JSON object schema with one field of the given
type and name (and an optional text documentation). A similar constructor,
defines an optional field in a JSON object.
The return type of
rectangleSchema is declared to be
JsonSchema[Rectangle], but we could have
used a more specific type:
Record[Rectangle]. This subtype of
additional operations such as
tagged (see the next section).
In the above example, we actually define two JSON object schemas (one for the
Record[Double], and one for the
height field, of type
and then we combine them into a single JSON object schema by using the
zip operation. Finally, we call the
to turn the
Record[(Double, Double)] value returned by the
zip operation into
It is also possible to define schemas for sum types. Consider the following type definition,
Shape, which can be either a
Circle or a
A possible JSON schema for this data type consists in using a JSON object with a discriminator
field indicating whether the
Shape is a
Rectangle or a
Circle. Such a schema can
be defined as follows:
(We have omitted the definition of
circleSchema for the sake of conciseness)
First, all the alternative record schemas (in this example,
tagged with a unique name. Then, the
orElse operation combines the alternative schemas into a
single schema that accepts one of them.
The result of the
tagged operation is a
Tagged[A] schema. This subtype of
JsonSchema[A] models a
schema that accepts one of several alternative schemas. It provides the
orElse operation turns the
Tagged[Rectangle] values into
Record[Either[Circle, Rectangle]], which is then, in this example, transformed into a
Record[Shape] by using
By default, the discriminator field is named
type, but you can use another field name either by
defaultDiscriminatorName method of the algebra, or by wrapping the
withDiscriminator call specifying the field name to use.
The examples above show how to use
xmap to transform a
JsonSchema[A] into a
case the transformation function from
B can fail (for example, if it applies additional
validation), you can use
xmapPartial instead of
In this example, we check that the decoded integer is even. If it is not, we return an error message.
There are different ways to represent enumerations in Scala:
- Sealed trait with case objects
- Third-party libraries, e.g. Enumeratum
For example, an enumeration with three possible values can be defined as a sealed trait with three case objects:
stringEnumeration in the
JsonSchemas algebra supports mapping the enum values to JSON strings.
It has two parameters: the possible values, and a function to encode an enum value as a string.
JsonSchema[Status] allows defining JSON members with string values that are mapped to
our case objects.
It will work similarly for other representations of enumerated values.
Most of them provide
values which can conveniently be passed into
However, it is still possible to explicitly pass a certain subset of allowed values.
JSON schemas for tuples from 2 to 22 elements are provided out of the box. For instance, if
there are implicit
JsonSchema instances for types
C, then you can summon
JsonSchema[(A, B, C)]. Tuples are modeled in JSON with arrays, as recommended in the
JSON Schema documentation.
Here is an example of JSON schema for a GeoJSON
Point, where GPS coordinates are modeled with a pair (longitude, latitude):
You can reference a currently being defined schema without causing a
by wrapping it in the
You can define a schema as an alternative between other schemas with the operation
Because decoders derived from schemas defined with the operation
orFallbackToliterally “fallback” from one alternative to another, it makes it impossible to report good decoding failure messages. You should generally prefer using
orElseon “tagged” schemas.
Schema descriptions can include documentation information which is used by documentation interpreters such as the OpenAPI interpreter. We have already seen in the first section that object fields could be documented with a description. This section shows two other features related to schemas documentation.
You can give names to schemas. These names are used by the OpenAPI interpreter to group the schema definitions at one place, and then reference each schema by its name (see the Swagger “Components Section” documentation).
named method to give a name to a
Tagged, or an
You can also include examples of values for a schema (see the
Swagger “Adding Examples” documentation).
This is done by using the
Applying the OpenAPI interpreter to this schema definition produces the following JSON document:
The module presented in this section uses Shapeless to generically derive JSON schemas for algebraic data type definitions (sealed traits and case classes).
With this module, defining the JSON schema of the
Shape data type is
reduced to the following:
genericJsonSchema operation builds a JSON schema for the given
type. The rules for deriving the schema are the following:
- the schema of a case class is a JSON object,
- the schema of a sealed trait is the alternative of its leaf case class schemas, discriminated by the case class names,
- each case class field has a corresponding required JSON object property of
the same name and type (for instance, the generic schema for the
Rectangletype has a
widthrequired property of type
- each case class field of type
Option[A]for some type
Ahas a corresponding optional JSON object property of the same name and type,
- documentation specific to case class fields can be defined by annotating the fields
- for sealed traits, the discriminator field name can be defined by the
@discriminatorannotation, otherwise the
defaultDiscriminatorNamevalue is used,
- the schema is named by the
@nameannotation, if present, or by invoking the
classTagToSchemaNameoperation with the
ClassTagof the type for which the schema is derived.
Here is an example that illustrates how to configure the generic schema derivation process:
In case you need to transform further a generically derived schema, you might want to use the
genericTagged operations instead of
genericJsonSchema. These operations
have a more specific return type than
genericRecord returns a
genericTagged returns a
The module also takes advantage shapeless to provide a more convenient
as operation for
transforming JSON schema definitions, instead of
An alternative to the module presented in the preceding section is provided as a third-party module: endpoints-json-schemas-macros.
Please see the README of that project for more information on how to use it and its differences with the module provided by endpoints.