Generate JSON Schema
The OpenAPI and JSON Schema backend ships as one WASM release bundle from finos/morphir-extensions, tagged
extension/openapi/v<version>. There is no public extension index yet, so you publish the bundle into a local repository withmorphir extension repository publishand install from it. This guide also covers the contract for a locally built extension. Rename the downloaded<artifact>.release.jsontorelease.jsonbefore publishing.
The morphir-openapi WASM extension turns public Morphir types into JSON
Schema 2020-12 documents. It also renders OpenAPI documents; see
Generate OpenAPI. Both targets come from the one
morphir-openapi extension, installed once. This guide covers the
json-schema target: openapi is documented separately because it adds
paths, operations, and a document-version choice that JSON Schema has no use
for.
The extension accepts Morphir IR v3 and v4. It projects public type definitions. It does not evaluate Morphir values or translate computation.
Install the published extension
Release bundles are published from
finos/morphir-extensions
under the tag extension/openapi/v<version>. Each release carries the .wasm
artifact, its .sha256 checksum, a <artifact>.release.json descriptor and the
descriptor's own .sha256. The artifact and the descriptor have build provenance
attestations. Releases up to 0.2.0 are in
finos/morphir-rust. There is no
public extension index yet, so you publish the bundle into a local repository
and install from it.
Download the artifact, its checksum and the descriptor into one directory,
verify the artifact's attestation, and rename the descriptor to release.json:
mkdir -p bundles/openapi
gh release download extension/openapi/v0.5.0 -R finos/morphir-extensions --dir bundles/openapi \
--pattern '*.wasm' --pattern '*.wasm.sha256' --pattern '*.release.json'
gh attestation verify bundles/openapi/morphir-openapi-0.5.0.wasm -R finos/morphir-extensions \
--signer-workflow finos/morphir-extensions/.github/workflows/extension-release.yml
mv bundles/openapi/*.release.json bundles/openapi/release.json
Then create a repository, register it, publish the bundle, and install:
morphir extension repository init repositories/local
morphir extension repository add local --directory repositories/local
morphir extension repository publish local --bundle bundles/openapi
morphir extension install --repository local morphir-openapi
morphir extension list
publish verifies the SHA-256 in release.json against the artifact and the
checksum file before it writes anything. The descriptor's targets list
carries both openapi and json-schema into the repository record, so one
installed extension serves both. install verifies the artifact again, stores
the module by content digest, and writes matching lock and catalog state. After that the extension runs offline: the repository
directory is only needed to install or update.
Set MORPHIR_HOME to an empty directory first to keep this out of your
regular home, and keep the same value when running generation.
Build and install a local extension
Contributors can build the bundle in
finos/morphir-extensions and
publish it the same way. The task writes the artifact, its checksum and
<artifact>.release.json into rust/target/bundle/openapi:
MISE_ENV=rust mise run rust:bundle openapi
Copy the artifact, its checksum and the descriptor (renamed to release.json)
into one directory, and publish it with
morphir extension repository publish as shown above. These commands
do not publish the extension anywhere public.
Run the generator
Once morphir-openapi is installed, generate JSON Schema documents with:
morphir generate --target json-schema --input morphir-ir.json --output generated/json-schema
Backend options live under [codegen.json-schema] in morphir.toml:
[codegen]
targets = ["json-schema"]
[codegen.json-schema]
unsupported = "error"
Repeat --option <KEY=VALUE> to override that table for one command:
morphir generate --target json-schema --option unsupported=warn-and-skip
The backend starts with its defaults, then applies [codegen.json-schema],
then applies CLI options in command-line order. The last CLI value for a key
wins. The CLI parses a value as JSON when possible; otherwise it passes a
string. Option names use snake_case.
The json-schema target reads only unsupported and the decimal_*
options (see Decimal) — version,
projection, result_responses, error_status, and operations all shape
paths, which JSON Schema documents never have. Option decoding runs before
the backend looks at the target, so a value one of these options accepts is
decoded and validated the same way for both targets; it is only the later use
of projection, result_responses, and operations to build paths that is
skipped for json-schema. A valid but target-irrelevant value — for example
--option projection=operations-public --target json-schema — is simply
ignored. An invalid value is not: error_status outside 400 through 599,
or an operations override path not starting with /, still fails
generation with JSC002 regardless of --target. See
Generate OpenAPI for what each of them controls.
Options and defaults
| Option | Accepted values | Default |
|---|---|---|
unsupported | error, warn-and-skip | error |
decimal_encoding | string, number | string |
decimal_precision | Integer from 1 through 1000; set it with decimal_scale | Not set |
decimal_scale | Integer from 0 through decimal_precision; set it with decimal_precision | Not set |
Unknown options, wrong JSON types, and invalid enum values fail with
JSC002.
Unsupported forms and partial output
The default unsupported = "error" is strict. Any projection error makes the
result unsuccessful and emits no artifacts. warn-and-skip omits the
unprojectable public form, emits a deterministic warning naming its Morphir
FQName, and returns only artifacts that remain independently valid.
The backend cannot safely project a function used as data, an open
extensible record, an opaque or incomplete type, an unbound type parameter, an
unresolved type reference, or a Dict with a non-String key.
kind is reserved for the same reason: a custom type with payload
constructors carries a kind discriminator, so a constructor argument that
projects to the property name kind would have to be both the argument and
the discriminator. That is a JSC003 naming the constructor; rename the
argument.
Type mapping
| Morphir form | JSON Schema representation |
|---|---|
Bool | {"type": "boolean"} |
Int | {"type": "integer", "format": "int64"} |
Float | {"type": "number", "format": "double"} |
String | {"type": "string"} |
Char | {"type": "string", "maxLength": 1} |
Unit | {"type": "null"} |
Decimal | {"type": "string", "format": "decimal", "pattern": ...}; see Decimal |
Maybe a | {"anyOf": [<a>, {"type": "null"}]} |
List a | {"type": "array", "items": <a>} |
Set a | {"type": "array", "items": <a>, "uniqueItems": true} |
Dict String a | {"type": "object", "additionalProperties": <a>} |
| Record alias | {"type": "object", "properties": {...}, "required": [...]} |
| Nullary custom type | {"type": "string", "enum": [...]} |
| Custom type with payload constructors | {"oneOf": [...]}, each variant an object carrying a kind discriminator fixed by const |
| Tuple | {"type": "array", "prefixItems": [...], "items": false, "minItems": n, "maxItems": n} |
Result error value | {"oneOf": [<Err object>, <Ok object>]}, each member's own field (error or value) holding the projected type, discriminated by kind |
Every Morphir record field is present; optionality is carried by the field's
own type (typically Maybe a), not by omitting the field. Every named schema
carries an x-morphir-fqname extension holding its canonical Morphir source
name, and a description when the declaration has Morphir documentation.
Names are derived deterministically from the canonical Morphir FQName —
UpperCamelCase for a schema name, lowerCamelCase for a field name — never
from traversal order, so two declarations that would produce the same schema
name are JSC004 name collisions rather than a silent rename.
A Dict with a non-String key has no object schema and fails with JSC003.
Decimal
From morphir-openapi 0.4.0, the Morphir SDK Decimal
(morphir/SDK:decimal#decimal) has a schema. Earlier releases refused IR
that uses Decimal with JSC003.
Examples in this section use this record from the extension's tests:
type alias Offer =
{ price : Decimal
, discount : Maybe Decimal
}
By default, a Decimal is a string that keeps the exact value. The price
field projects to:
{
"type": "string",
"format": "decimal",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
}
The pattern accepts plain decimal notation, as Morphir data codecs write it,
for example "12.50", "0" and "-0.05". It refuses a JSON number,
an exponent ("1e3"), a + sign and ".5".
Set decimal_encoding to number to write a Decimal as a JSON number.
Most consumers read a JSON number as binary floating point, so this encoding
can lose the exact value:
[codegen.json-schema]
decimal_encoding = "number"
{ "type": "number", "format": "decimal" }
Set decimal_precision and decimal_scale together to bound the digits of
the string pattern. They have the same meaning as in the Avro extension.
With decimal_precision = 10 and decimal_scale = 2, price projects to:
{
"type": "string",
"format": "decimal",
"pattern": "^-?(0|[1-9][0-9]{0,7})(\\.[0-9]{1,2})?$"
}
These combinations fail with JSC002: only one of the two bounds, a bound
with decimal_encoding = "number", a precision outside 1 through 1000, or a
scale larger than the precision.
Diagnostic codes
| Code | Meaning |
|---|---|
JSC001 | The host asked for a target this extension does not advertise |
JSC002 | A backend option was unknown, of the wrong type, or out of range |
JSC003 | A Morphir form has no safe schema projection |
JSC004 | Two projected declarations claimed the same schema name |
File layout
One JSON Schema document is written per public root type. Its name is
<module path, lowercased and dot-joined>.<SchemaName>.schema.json — for
example, a Customer type declared in the customer module becomes
customer.Customer.schema.json. Each document is
self-contained: $schema is
https://json-schema.org/draft/2020-12/schema, $id is
<SchemaName>.schema.json, title is the schema name, and $defs holds
exactly the transitive closure of definitions the root's own $refs reach —
no unused definition, no dangling reference.
For runtime boundaries and release status, see the accepted WASM extension runtime and Avro backend proposal and the OpenAPI and JSON Schema backend proposal.