The code generator will make intelligent choices for most aspects of the generation process, and in many cases the defaults chosen will be ideal, or at least acceptable. But for many situations, additional configuration is required – for example, to nominate the name to be used for a generated class, or to specify how a non-standard extension to the JSON Schema syntax is to be handled.
All of this functionality is accessible through the CodeGenerator
object, but the configuration file provides a
concise and convenient means of customising the code generation for a project.
The config file may take the form of a JSON or YAML file, or may be supplied as a parsed JSON object.
The use of the configuration file is simple – just provide the file to the configure()
function of the
CodeGenerator
:
val codeGenerator = CodeGenerator()
codeGenerator.configure(File("/path/to/config.json"))
codeGenerator.baseDirectoryName = "output/directory"
codeGenerator.generate(File("/path/to/example.schema.json"))
The configuration file includes the following:
title
version
description
targetLanguage
packageName
markerInterface
generatorComment
additionalPropertiesOption
examplesValidationOption
anddefaultValidationOption
nestedClassNameOption
derivePackageFromStructure
extensionValidations
nonStandardFormat
customClasses
decimalClassName
classNames
annotations
companionObject
Adds a title to the configuration (optional – for documentation purposes only). If present, the value must be a string.
Adds a version id to the configuration (optional – for documentation purposes only). If present, the value must be a string.
Adds a description to the configuration (optional – for documentation purposes only). If present, the value must be a string.
The default target language is Kotlin; to change this, the targetLanguage
property may be used:
{
"targetLanguage": "java"
}
The values allowed are kotlin
, java
or typescript
.
New in version 0.80: Java output now includes Builder
classes, to aid with creation of classes with long
constructor parameter lists.
The package name may be specified as a configuration option:
{
"packageName": "com.example.data"
}
The value must be a non-empty string, or null
to specify that no package name is to be used (this is the default).
The code generator allows a “marker” interface to be added to each generated class. This may be specified as a configuration option:
{
"markerInterface": "com.example.Model"
}
The value must be a non-empty string, or null
to specify that no marker interface is to be used (this is the default).
The generator comment (added to the comment block at the start of each generated file) may be specified as a configuration option:
{
"generatorComment": "Generated from v1.1 of the schema"
}
The value must be a non-empty string, or null
to specify that no generator comment is to be used (this is the
default).
This option may be used to enable the use of the JSON Schema additionalProperties
validation, along with
patternProperties
, minProperties
and maxProperties
.
For more information see the additionalProperties
and patternProperties
guide, but in summary:
The use of additionalProperties
or patternProperties
requires the generated class to handle object properties with
names that are not known in advance.
The best way of accommodating this is for the generated class to implement the Map
interface, but this results in code
that is very much more complicated than it otherwise would be, and for this reason, this form of code generation is used
only when the additionalPropertiesOption
is set to strict
:
{
"additionalPropertiesOption": "strict"
}
The default is ignore
, which causes additionalProperties
and patternProperties
to be ignored.
The JSON Schema specification says, of examples
and default
: “It is RECOMMENDED that these values be valid
against the associated schema.”
The code generator allows for the optional validation of examples
and default
entries; either or both of
examplesValidationOption
and defaultValidationOption
may be set to:
none
: no validation will be performed (this is the default)warn
: warning messages will be logged for any validation errorsblock
: block the code generator from continuing if any validation errors are encountered
For example:
{
"examplesValidationOption": "warn",
"defaultValidationOption": "block"
}
Consider the following schema:
{
"$schema": "http://json-schema.org/draft/2019-09/schema",
"$id": "http://example.com/test-1",
"type": "object",
"properties": {
"name": {
"$ref": "#/$defs/NameType"
}
},
"$defs": {
"NameType": {
"type": "object",
"properties": {
"givenName": {
"type": "string"
},
"surname": {
"type": "string"
}
}
}
}
}
When the code generator needs to create a nested class for the name
property, there are two options for choosing the
name of that class:
- when the property references another schema by means of a
$ref
as in this example, use a name derived from the reference (in this case that will beNameType
) - derive the name from the property name (in this case,
Name
– the capitalised form ofname
)
The naming option may be specified by:
{
"nestedClassNameOption": "refSchema"
}
The values allowed are refSchema
(this is the default) or property
.
For greater flexibility in naming nested classes, see the classNames
section below.
When generating a classes from a set of schema files in a directory structure, the code generator will optionally use
the directory structure of the schema files to determine the package to be used for the generated classes.
This option (default true
) may be configured by the use of the derivePackageFromStructure
setting:
{
"derivePackageFromStructure": false
}
The JSON Schema specification allows for extensions – additional keywords denoting aspects of the schema description not covered by the general specification. For example, an organisation may have a defined set of "domain primitives" – small immutable objects that are used frequently throughout the organisation's IT systems – and may wish to use JSON Schema extensions to simplify the use of these types:
{
"$schema": "http://json-schema.org/draft/2019-09/schema",
"$id": "http://example.com/test-2",
"type": "object",
"properties": {
"amount": {
"type": "string",
"x-local-type": "money"
},
"currency": {
"type": "string",
"x-local-type": "currency"
}
}
}
The code generator provides two separate mechanisms for dealing with JSON Schema extensions:
- extension validations, where an extension is declared to be the equivalent of other schema constructs, or
- custom classes, making use of pre-existing classes that provide the required functionality.
The second option is described below (see customClasses
); the first describes the type in terms of
regular JSON Schema syntax:
{
"extensionValidations": {
"x-local-type": {
"money": {
"type": "string",
"pattern": "^[0-9]{1,9}\\.[0-9]{2}$"
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$"
}
}
}
}
This specifies an extension x-local-type
with two possible values:
money
, which causes a pattern validation for decimal strings values to be added to the propertycurrency
, which does the same with a 3-character alphabetic pattern validation.
The schema object (the JSON object following money
or currency
in the above example) may contain any form of
validation, and as well as the pattern validations as shown here, typical uses might include a minimum
of zero (to
disallow negative values), a minLength
of 1 (to enforce the use of non-empty strings) or a format
of uuid
(to
require that an application-specific id is always a UUID).
The validations provided in this way are added to any existing schema definitions at the point where the extension
appears.
This may have the effect of causing the generator to output initialisation validations, or in some cases may affect the
choice of generated class for a property – for example, adding a format validation may cause the generator to use
a standard class like UUID
or LocalDate
.
There is no limit to the number of extension keywords or the values allowable for each keyword, but only string values
may be specified using this mechanism.
More complex extensions will still require configuration code to configure the CodeGenerator
object.
The format
construct in JSON Schema may be used with non-standard format keywords, and many users will prefer this
over defining an extension keyword and value.
Using this approach, the schema definition from the earlier example becomes:
{
"$schema": "http://json-schema.org/draft/2019-09/schema",
"$id": "http://example.com/test-3",
"type": "object",
"properties": {
"amount": {
"type": "string",
"format": "money"
},
"currency": {
"type": "string",
"format": "currency"
}
}
}
Again, there are two alternative mechanisms for configuring the code generator to work with non-standard format
keywords.
And again, the second option is described under customClasses
; the first uses regular JSON Schema
syntax, this time on an individual format:
{
"nonStandardFormat": {
"money": {
"type": "string",
"pattern": "^[0-9]{1,9}\\.[0-9]{2}$"
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$"
}
}
}
This specifies two new format
keywords, money
and currency
which act like the extension definitions above.
And as with extensions, there are no limits on the number of additional keywords defined, or on the types of schema
definitions used.
It is even possible to define a format as mapping to another format – for example, specifying a new keyword
x-iso8601-date
as a schema containing "format": "date"
will cause the code generator to use LocalDate
for any
properties using that format (assuming the format for date
has not itself been overridden).
The code generator will decide on the data type for the properties of an object or the members of an array based on a
set of built-in rules.
For example, a Kotlin String
will be generated for a property of type string
, unless the property also has a
format
, in which case the type chosen may be LocalDate
or UUID
or one of a number of other known types.
In most cases, the choices made by the generator will be exactly what the user wants, but in some cases there will be a need to specify the use of nominated types for certain properties or array items.
To revisit the example of money
and currency
data types – many organisations will have their own classes to
hold values of these types, and they will require the generated code to make use of these classes.
The JSON Schema extensions described above can be used to indicate that a custom class is to be used, for example:
{
"customClasses": {
"extension": {
"x-local-type": {
"money": "com.example.util.Money",
"currency": "com.example.util.Currency"
}
}
}
}
This will cause all properties that contain these extension constructs to be generated as references to the specified classes. If the generated classes are to be used in conjunction with automated JSON serialisation and deserialisation, it is the responsibility of the user to provide custom functions to handle this.
The use of non-standard format
keywords can also be used to specify the generation of custom class references:
{
"customClasses": {
"format": {
"money": "com.example.util.Money",
"currency": "com.example.util.Currency"
}
}
}
This mechanism can also be used to specify alternative classes for formats that would normally cause the generator to
use one of its inbuilt mappings.
For example, even though the Joda Time library has long been deprecated, many organisations still have a considerable
investment in software that uses that library, so an implementation may wish to direct the code generation to use Joda
Time classes for date
or date-time
formats.
The most fine-grained control of custom class selection can be achieved by specifying the location of the property in the schema by means of its URI (including fragment locator):
{
"customClasses": {
"uri": {
"https://example.com/demonstration/account.schema.json#/properties/balance": "com.example.util.Money",
"https://example.com/demonstration/account.schema.json#/properties/currency": "com.example.util.Currency"
}
}
}
It could be very tedious to specify every occurrence of a particular type individually, but the technique also applies
to definitions included by means of a $ref
.
Specifying the URI of the referenced definition will cause the nominated class to be used for all references to the
definition.
This use of URI to specify custom class selection has the distinct advantage that it may be used in conjunction with schema files that are not open to modification, for example schema files that are read directly from public websites.
NOTE: If a default value is given for a property that maps to a custom class, the code generator will output a constructor for the custom class, with the default value as a single parameter. If no such constructor exists, default values should be avoided.
For non-integer decimal number properties. the code generator will use the class java.math.BigDecimal
.
When generating code for use in a Kotlin Multi-Platform environment this can be a problem, since this class exists only
in the JVM environment.
The decimalClassName
option allows the name of a substitute decimal class to be specified.
There are a number of constraints on a class used for this purpose:
- It must have a constructor taking a
String
which may hold any valid JSON number value. - It must implement
Comparable<itself>
. - It must have the constant values
ZERO
andONE
.
For example:
{
"decimalClassName": "com.example.decimal.Decimal"
}
Note that the fully-qualified class name must be specified.
The code generator will attempt to choose names for generated classes based on the $id
of the schema.
On most occasions this will lead to satisfactory results, but in many cases, users will wish to specify the class name
to be used for the generated code.
{
"classNames": {
"urn:jsonschema:com:example:Person": "Person"
}
}
This will cause the schema definition with the specified $id
to be generated as a class with the name given.
Note that in this case, the class name is not a fully-qualified class name – the package name used will be the
one specified with the packageName
configuration option (possibly extended by the directory structure
– see the derivePackageFromStructure
option).
From version 0.106 onward, this has been extended to allow the naming of nested classes. This requires the specification of the full URI, including the “fragment” containing the path to the schema. For example, given this schema:
{
"$schema": "http://json-schema.org/draft/2019-09/schema",
"$id": "http://example.com/test-9",
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"addr": {
"type": "object",
"properties": {
"line1": {
"type": "string"
},
"line2": {
"type": "string"
}
}
}
}
}
Without additional configuration, the property addr
would be generated as a nested class named Addr
.
To name the nested class Address
, add the following to the configuration:
{
"classNames": {
"http://example.com/test-9#/properties/addr": "Address"
}
}
Note that the full URI of the nested class is the URI of the enclosing schema, followed by #
and the JSON path to the
nested schema.
When the path contains special characters, for example when specifying a patternProperties
schema, the special
characters must be escaped using the URI encoding scheme.
For example:
{
"classNames": {
"http://example.com/test-8#/patternProperties/%5e%5bA-Z%5d%7b3%7d%24": "Currency"
}
}
The code generator can be configured to add annotations to the generated classes and fields (properties). The annotation may be a simple marker annotation with no parameters, or may have parameters formed from a Mustache template.
For example, to add a @Generated
annotation to each class:
{
"annotations": {
"classes": {
"javax.annotation.Generated": "\"{{&generator}}\", date=\"{{&dateTime}}\""
}
}
}
This will result in an annotation similar to the following being added to each class:
import javax.annotation.Generated
@Generated("net.pwall.json.schema.codegen.CodeGenerator", date="2022-07-18T10:36:18.626+10:00")
The example illustrates several important points:
- Class annotations are defined in a section headed
classes
(field annotations are in a section headedfields
). - The annotation name must be supplied as a fully-qualified class name.
The fully-qualified name will be used on an
import
, and just the class name will be used on the annotation itself. - The parameters for the annotation must be supplied together as a single string, and any special characters like quotes must be escaped with backslashes as is normal for JSON (when using YAML, single-quoted strings allow any special characters other than single quote itself). No syntax checking is performed on the parameters, and any errors may result in code that will not compile. The template must not include the parentheses surrounding the parameters; these will be added by the generator when required.
- The context object used during Mustache template processing contains several variables that may be useful. Some of these are listed below.
A second example shows a field annotation with no parameters:
{
"annotations": {
"fields": {
"com.example.anno.Demo": null
}
}
}
This will result in the simple annotation @Demo
being added to each field with no parameters (and no parentheses),
with an import com.example.anno.Demo
added to the start of the file.
This example shows:
- Field annotations are defined in a section headed
fields
. - The absence of parameters is indicated by a
null
template string.
The Mustache template expansion can take values from a "context object". For class annotations, the context object contains the following:
Name | Type | Description |
---|---|---|
className |
String |
the class name |
packageName |
String? |
the package name (or null ) |
source |
String |
the schema source file name (if available) |
schema |
JSONSchema |
the schema as a JSONSchema |
For field annotations, the context object contains:
Name | Type | Description |
---|---|---|
name |
String |
the field name |
kotlinName |
String |
the name, usable as a Kotlin variable name (see below) |
javaName |
String |
the name, usable as a Java variable name (see below) |
isObject |
Boolean |
true if the field type is object |
isArray |
Boolean |
true if the field type is array |
isString |
Boolean |
true if the field type is string |
isBoolean |
Boolean |
true if the field type is boolean |
isDecimal |
Boolean |
true if the field type is decimal |
isIntOrLong |
Boolean |
true if the field type is integer |
isInt |
Boolean |
true if the field type is integer and will fit in an Int (see below) |
isLong |
Boolean |
true if the field type is integer and will not fit in an Int |
isRequired |
Boolean |
true if the field appears in a required constraint |
The kotlinName
will be the same as the name
, except when it contains characters unacceptable in a Kotlin variable
name (such as spaces) or when it is a reserved word (like val
).
In these cases, the kotlinName
is the original name enclosed in backticks.
Java does not have the backtick mechanism, so javaName
is the original name with unacceptable characters replaced by
underscore, and names clashing with reserved words being suffixed with underscore.
A field that is declared to be an integer
will be generated as a Long
, unless there are minimum
and maximum
constraints limiting it to 32 bits (signed).
The Boolean
values may be used in a Mustache conditional "section", as follows:
{
"annotations": {
"fields": {
"com.example.anno.Demo": "\"Field {{name}} is {{^isString}}not {{/isString}}a String\""
}
}
}
Mustache name resolution uses a chain of context objects; if a name is not found in the current context object, resolution switches to the parent context object, then to the parent's parent, and so on. For both class annotations and field annotations, the parent context object provides information on the current code generation run:
Name | Type | Description |
---|---|---|
dateTime |
OffsetDateTime |
the date/time of the current generation run |
date |
LocalDate |
the date portion of the above date/time |
time |
LocalTime |
the time portion of the above date/time |
generator |
String |
the code generator name, suitable for use in @Generated |
uuid |
UUID |
a random UUID, for tagging a specific build |
The use of this information is shown in the first example in the annotations
section.
It can be useful to create "factory" functions to instantiate the generated classes, using extension functions on the
companion object of the class.
For example, to create an instance of a generated class Person
:
fun Person.Companion.create(name: String): Person {
// create a Person with the supplied name
}
This requires that the generated class have a companion object, and that is not always the case.
To force the output of a companion object for one or more specified classes, or for all classes, the companionObject
configuration setting may be used.
This setting takes either a boolean, to enable or disable the output of companion objects for all classes, or an array
of strings nominating the individual classes.
To select this option for all classes:
{
"companionObject": true
}
Or for a selected set of classes:
{
"companionObject": [ "Class1", "Class2" ]
}
(just the class name, not the fully-qualified name including package, must be specified)