Skip to main content

Generate OpenAPI

The OpenAPI and JSON Schema backend ships as one WASM release bundle from finos/morphir-rust, tagged extension/openapi/v<version>. There is no public extension index yet, so you publish the bundle into a local repository with morphir extension repository publish and install from it. This guide also covers the contract for a locally built extension. Rename the downloaded <artifact>.release.json to release.json before publishing.

The morphir-openapi WASM extension turns a public Morphir package into an OpenAPI document: its public types become components/schemas, and, depending on the chosen projection mode, its public value specifications become paths operations. It also renders standalone JSON Schema documents; see Generate JSON Schema. Both targets come from the one morphir-openapi extension, installed once — install it once and both --target openapi and --target json-schema are available.

The extension accepts Morphir IR v3 and v4. It projects types and value specifications. It does not evaluate Morphir values or translate computation.

Install the published extension​

Release bundles are published from finos/morphir-rust under the tag extension/openapi/v<version>. Each release carries three assets: the .wasm artifact, its .sha256 checksum, and a <artifact>.release.json descriptor. There is no public extension index yet, so you publish the bundle into a local repository and install from it.

Download the assets into one directory, rename the descriptor to release.json, and keep only those three files in the directory:

mkdir -p bundles/openapi
gh release download extension/openapi/v0.1.0 -R finos/morphir-rust --dir bundles/openapi
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 from ecosystem/morphir-rust with its packaging task and publish it the same way. The task writes release.json, the artifact, and the checksum into one directory:

mise run extension:artifact:openapi

Publish .morphir/build/extensions/openapi 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 the default OpenAPI document with:

morphir generate --target openapi --input morphir-ir.json --output generated/openapi

The generator writes a single artifact, openapi.json, into the output directory. Backend options live under [codegen.openapi] in morphir.toml:

[codegen]
targets = ["openapi"]

[codegen.openapi]
unsupported = "error"
version = "3.1"
projection = "schemas"
result_responses = "data"
error_status = 400

[codegen.openapi.operations."acme/customer:domain#find-customer"]
method = "get"
path = "/customers/{id}"

[codegen.openapi.operations."acme/customer:domain#find-customer".parameters]
id = "path"

Repeat --option <KEY=VALUE> to override that table for one command:

morphir generate --target openapi \
--option projection=operations-public \
--option result_responses=split \
--option error_status=422

The backend starts with its defaults, then applies [codegen.openapi], 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.

version is the one option where that JSON-first rule bites. Its values are the strings "3.1" and "3.0", but 3.0 and 3.1 are also valid JSON numbers, so the CLI hands the backend a number and option decoding fails with JSC002. Quote the value so it reaches the backend as a string, and quote the whole argument so the shell keeps the inner quotes:

morphir generate --target openapi --option 'version="3.0"'

--option version=3.0 fails. In morphir.toml no extra quoting is needed: TOML's version = "3.0" is already a string.

Options and defaults​

OptionAccepted valuesDefault
unsupportederror, warn-and-skiperror
version"3.1", "3.0" (strings; on the CLI write --option 'version="3.0"')"3.1"
projectionschemas, operations-entry-points, operations-publicschemas
result_responsesdata, splitdata
error_statusInteger from 400 through 599400
operationsMap from an exact Morphir FQName to a per-operation overrideEmpty map

Unknown options, wrong JSON types, invalid enum values, an error_status outside 400–599, and an operations override path that does not start with / all fail option decoding with JSC002 — this check runs before generation, against the option value alone, so it does not need to know whether the override's key names a real value specification. Each operations override key must separately name a value specification the package actually declares, or generation fails with OAS002.

Choose what to project​

The three projection modes answer different questions. schemas only ever populates components/schemas — paths is still emitted, as an empty object, since some validators require the key even when there is nothing in it.

Projectioncomponents/schemaspaths
schemasPublic type rootsEmpty
operations-entry-pointsPublic type roots, plus any type an operation reachesDeclared entry points from a v4 Application only
operations-publicPublic type roots, plus any type an operation reachesEvery public value specification

A Library or a Specs distribution has no declared application entry points, so operations-entry-points produces an operation-free paths for them — it never invents operations from ordinary public values.

Default HTTP mapping and per-operation overrides​

Every selected value specification starts from the same default mapping, before any override is applied:

  • HTTP method: POST.
  • Path: /<module segments, lowercased and slash-joined>/<value name, lowerCamelCase>. For example, acme/customer:domain#find-customer becomes /domain/findCustomer.
  • Request body: every input becomes a required application/json property. A zero-argument constant has no request body at all.
  • Response: the output type becomes the 200 response, described as "Successful result".

Each operation also carries operationId (everything after the package's : — every module segment and the local value name — run together and lowerCamelCased: acme/customer:domain#find-customer becomes domainFindCustomer), x-morphir-fqname naming its exact Morphir source, and x-morphir-value-kind, either constant or function. A declared entry point additionally carries x-morphir-entry-point: true, x-morphir-entry-point-id, and a lowercase x-morphir-entry-point-kind of main, command, or handler.

options.operations, keyed by the operation's canonical Morphir FQName, overrides this default. A worked example, moving findCustomer's id input onto a path parameter:

[codegen.openapi.operations."acme/customer:domain#find-customer"]
method = "get"
path = "/customers/{id}"

[codegen.openapi.operations."acme/customer:domain#find-customer".parameters]
id = "path"

method replaces the default POST. path replaces the default path template. Each entry under parameters moves one named request field out of the request body: path binds it to a {name} path placeholder (which must already appear in the resulting path, or the generation fails with OAS002), query binds it to a query parameter, header binds it to a request header, and body leaves it in the request body — useful for restating a field explicitly without moving it. The check runs both ways: a {name} placeholder in the resulting path with no path-bound parameter to fill it also fails with OAS002, since OpenAPI requires every path variable to have its own in: path parameter. Setting path without the matching parameters entry is the easiest way to trip this. Every moved parameter is still required: moving where a value is carried never makes it optional. A query or header binding whose name matches no request field is silently ignored, since neither location renders a path placeholder and leaving the field in the request body is a safe default.

A path claimed by two operations, or an operationId claimed by two operations, fails with OAS001 naming both Morphir FQNames — this is always an error, regardless of unsupported, because it is a genuine ambiguity in the projected package rather than a form the backend cannot represent.

result_responses and error_status​

Morphir's Result error value is a common output type. result_responses decides how it becomes HTTP responses:

  • data (the default) keeps the whole Result as one 200 response body: a discriminated choice between an Err object (holding the error under an error field) and an Ok object (holding the success value under a value field), both tagged by a kind property.
  • split projects the Ok member's own type directly as the 200 response and the Err member's own type as a separate error response, at the status code error_status names.

Because a discriminated choice writes a kind property into every variant, kind is reserved: a custom-type constructor whose argument projects to the property name kind has no safe schema and fails with JSC003 naming the constructor. Rename the argument.

Result is detected by its exact Morphir source name (morphir/SDK:result#result), never by shape, so a package-local type that happens to look like an error/value choice is never mistaken for it and never split. An output type that is not Result-shaped ignores result_responses entirely and always becomes one 200 response.

error_status is any integer from 400 through 599 and defaults to 400. It only has an effect when result_responses = "split" and at least one projected operation's output is Result-shaped.

The version option and the OpenAPI 3.0 downgrade​

version takes the strings "3.1" and "3.0", never the bare numbers. On the CLI that means --option 'version="3.0"'; --option version=3.0 parses as a JSON number and fails with JSC002.

version = "3.1" (the default) renders "openapi": "3.1.0" — the document built from the projection, unchanged. version = "3.0" renders "openapi": "3.0.3": the same document is always built as 3.1 first, then rewritten, so there is one projection and one document builder and the two versions cannot drift apart. The rewrite replaces every 2020-12-only form the 3.0 dialect (JSON Schema Draft 4-based) does not accept:

  • {"const": v} becomes {"enum": [v]} — a discriminated variant's kind property keeps its exact value, just spelled as a single-value enum instead of const, which 3.0 does not have.
  • A field or output typed Maybe a — {"anyOf": [<a>, {"type": "null"}]} in 3.1 — becomes <a> merged with "nullable": true.
  • A bare Unit ({"type": "null"}, not part of any Maybe) becomes {"nullable": true, "enum": [null]}, since OAS 3.0.3 §4.4 has no null type at all and there is no other type for nullable to sit beside.
  • A tuple ({"prefixItems": [...], "items": false}) becomes an array whose items is {"anyOf": [...]} over the tuple's member schemas, with minItems/maxItems still pinning the length exactly. This uses anyOf, not oneOf: two tuple members can share a schema (for example (Int, Int)), and oneOf would then reject every element, since it requires exactly one branch to match.
  • A $ref that sits next to another keyword — original or produced by one of the rewrites above — becomes {"allOf": [{"$ref": ...}], ...siblings}, because 3.0 tooling ignores every sibling of a $ref.
  • x-morphir-* extension keys are valid in 3.0 and are never touched.

These rewrites only ever fire on the keywords of a Schema Object — never on the keys of a properties map or a components/schemas map, both of which hold arbitrary Morphir-derived names. A record field genuinely named const survives untouched.

Limitation: a nullable reference​

A field or output typed Maybe SomeNamedType — a Maybe over a named type, not a scalar — downgrades to {"allOf": [{"$ref": ...}], "nullable": true}. OAS 3.0.3 §4.7.24.2 only extends the allowed types when a type keyword sits in the same schema object as nullable, and wrapping a $ref in allOf never adds one, so most 3.0 tooling ignores nullable there and a generated client may reject null for a field where Morphir data allows it. There is no fully correct 3.0 encoding for this shape: inlining the referenced schema would preserve nullability but duplicates schemas and breaks on recursive types. The downgrade keeps the allOf form and instead records a JSC003 warning naming the schema or property, so the gap is visible rather than silent. Generate with version = "3.1" instead if a consumer must enforce the null case.

Diagnostic codes​

CodeMeaning
JSC001The host asked for a target this extension does not advertise
JSC002A backend option was unknown, of the wrong type, or out of range
JSC003A Morphir type, value signature, or operation has no safe projection
JSC004Two projected declarations claimed the same schema name
OAS001Two synthesized operations claimed the same path and method, or the same operationId
OAS002An operations override names no declared value specification, one of its Path-bound parameters has no matching {name} placeholder in the operation's path, or the operation's path carries a {name} placeholder that no Path-bound parameter fills

Under the default unsupported = "error", any JSC003 fails the whole generation and writes no artifact. Under unsupported = "warn-and-skip", an operation whose own signature cannot be projected is dropped from paths with a JSC003 warning naming its Morphir FQName, and the rest of the document still renders; the same rule follows one hop further out, so an operation that only references a type dropped elsewhere in the document is also dropped rather than left pointing at a missing schema. OAS001 and OAS002 are always errors, regardless of unsupported: both name a mistake in the Morphir source or the configuration itself, not a form the backend cannot represent. Under version = "3.0", a nullable reference (see above) also emits a JSC003 warning, unconditionally on unsupported — this warning never fails generation, since it flags a real limitation of the 3.0 dialect itself, not a Morphir form the backend refuses to project.

For runtime boundaries and release status, see the accepted WASM extension runtime and Avro backend proposal and the OpenAPI and JSON Schema backend proposal.