Command line flags
Every flag below is also a key in a config file, which is what a project
generating several crates wants: shared values are declared once, paths resolve against the config
file rather than the current directory, and cddl-codegen --config <file.toml> generates every
crate in one run. --config is mutually exclusive with the flags below — which is why
--print-flags exists: it prints the
flags each crate would be generated with, keyed by the config line that produced each one, and
generates nothing.
--inputSpecifies the input CDDL file(s).
For a single file:
cddl-codegen --input examples/test.cddl --output export
If a directory is specified e.g. --input=some_dir then it will read all files in this directory (non-recursively).
The output format changes here. If there's a lib.cddl the types contained there are the standard output , and any other file e.g. foo.cddl will have its own module foo/mod.rs with its own foo/serialization.rs, etc.
cddl-codegen --input examples --output export
--outputSpecifies the output directory.
cddl-codegen --input examples --output export
--static-dirChanges the directory used for cddl-codegen's static runtime and template files. The default is static.
cddl-codegen --input=example --output=export --static-dir custom-static
In a config file this is static-dir = "…", and it is one of the two generation flags that may also
be passed alongside --config (the other is --verbosity) — where it applies to every crate in the
run and overrides the key. See the config file reference.
--lib-nameSpecify the rust crate name for the output library. The wasm crate will have -wasm appended.
cddl-codegen --input=example --output=export --lib-name some-crate-name
If the output lands inside a cargo workspace that already has a member of the same name, generation warns on stderr naming both manifests — cargo would otherwise report two packages named X in this workspace at your next build, long after the run that caused it reported success. It is a warning and not a refusal: detecting it means reading the surrounding workspace, which is an input the tool is free to read but must never let change a generated byte.
--annotate-fieldsIncludes additional field-location context in generated deserialization errors. On by default; disabling it can slightly reduce generated code size.
Possible values: true, false
cddl-codegen --input=example --output=export --annotate-fields false
--to-from-bytes-methodsGenerates to_cbor_bytes() / from_cbor_bytes() methods on all WASM objects. On by default.
(The rust code doesn't need this as you can directly use the Serialize/Deserialize traits on them.)
Possible values: true, false
cddl-codegen --input=example --output=export --to-from-bytes-methods true
--wasmWhether to output a wasm crate. On by default.
Possible values: true, false
cddl-codegen --input=example --output=export --wasm false
--componentWhether to output a WebAssembly component crate (component/) beside the rust crate: a generated WIT package describing the spec's types, plus the wit-bindgen guest glue that implements it over the rust crate. Off by default.
It is a third face, independent of --wasm — both may be on, and neither implies the other — and the two serve different consumers:
--wasmproduceswasm-bindgenclasses for a JavaScript host.--componentproduces awasm32-wasip2component whose surface is a typed WIT contract. That contract composes with other components and is consumable from any component-model host, not only from JS.
The generated crate is pure glue: all CBOR logic stays in the rust crate, which it takes as a path dependency without the --rust-wasm-feature feature — #[wasm_bindgen] attributes emit __wbindgen_* imports that componentization cannot resolve on wasip2.
The whole boundary — what crosses as a handle, what crosses as a value, and why — is documented in Component differences. Two API deltas from the wasm face are worth knowing before you even choose the flag:
- Collections cross the boundary as plain
list<…>, not as wrapper classes: a consumer builds a native array instead of callingFooList.new()/add(). The ownership hazard those wrappers exist to prevent does not arise here, because every parameter position takes aborrow<…>rather than transferring a handle. - Returned values are snapshots. A getter hands back a fresh handle over a clone of the field, so mutating what you got does not mutate the parent — the same clone-at-the-boundary rule the wasm face already follows.
It also constrains what the spec may contain, because WIT is stricter about names than Rust. Each exported module scope becomes one WIT interface, and interfaces linked with use must be acyclic: a multi-file spec whose scopes reference each other in a cycle generates fine without this flag and is rejected with it, naming the scopes and the references that close the cycle. The remedy is to move a type so the scopes are acyclic, or to put the spec in a single file.
Known limitation — narrow the rust crate's crate-type before building for wasm32-wasip2. The generated rust/Cargo.toml declares crate-type = ["cdylib", "rlib"], because the wasm face needs the cdylib for wasm32-unknown-unknown. On wasm32-wasip2 that same cdylib can crash the linker in the rust sysroot:
error: linking with `wasm-component-ld` failed: exit status: 1
error: failed to invoke LLD: signal: 11 (SIGSEGV) (core dumped)
That is a crash inside wasm-component-ld/LLD, not a diagnostic about your spec, and it is spec-dependent — a multi-scope spec reproduced it under rustc 1.96.1 while several single-file specs built cleanly. Building only the component/ crate does not avoid it: cargo builds every crate-type a path dependency declares, so the cdylib is linked either way.
The component face never needs that cdylib — it links the rust crate as an rlib — so narrow the dependency:
# rust/Cargo.toml
[lib]
crate-type = ["rlib"]
Do this in the consuming workspace rather than expecting the tool to emit it: the cdylib is load-bearing for --wasm, and a crate generated with both faces on needs it. This project's own wasm32-wasip2 build gate applies exactly this narrowing before compiling a generated component crate.
Possible values: true, false
cddl-codegen --input=example --output=export --component true
--wit-packageThe identifier of the generated WIT package: <namespace>:<name>[@<version>].
Default: cddl:<--lib-name, kebab-cased>@0.1.0. Both sides of the : are WIT identifiers — lowercase ASCII words joined by - — and the version is optional, defaulting to 0.1.0.
Choose it deliberately, because it is the linking identity of everything the component exports. Two components unify a type only when the package id, the interface name and the type's shape all agree, so renaming the package is a composition-breaking change on exactly the same footing as changing a type.
Versions link by semver, and semver's 0.x rule applies. A host resolves an import of a:b/c@0.2.0 against a defined 0.2.1, but not against 0.3.0 — so on a 0.x line a minor bump is a link-time break. For wire types that is arguably what you want (an incompatible API fails to link rather than misbehaving at runtime), as long as it is a decision rather than a surprise.
Requires --component=true; without it no .wit is emitted for the value to title, so the combination is rejected up front.
cddl-codegen --input=example --output=export --component=true \
--wit-package=acme:ledger@0.2.0
--rust-wasm-featureNames the cargo feature the generated rust crate's only #[wasm_bindgen] attribute is gated behind. That attribute is emitted solely on c-style enums (all-fixed-value unions), which are exposed to wasm directly rather than through a wrapper type — see Output format. Under --wasm the attribute is emitted as #[cfg_attr(feature = "<name>", wasm_bindgen::prelude::wasm_bindgen)], wasm-bindgen is an optional dependency of the rust crate, and the [features] table always carries <name> (as <name> = ["dep:wasm-bindgen"] when a c-style enum exists, otherwise <name> = []). The rust crate therefore compiles standalone with the feature off; the generated wasm crate enables it via its path dependency ({ path = "../rust", features = ["<name>"] }), so wasm builds are unchanged.
The name is only relevant to consumers who compile the rust crate into their own wasm-bindgen build without going through the generated wasm crate — set it to match the feature their manifest already gates wasm-bindgen on (e.g. --rust-wasm-feature=used_from_wasm).
The value must be a valid cargo feature name (non-empty, characters in [A-Za-z0-9_+.-]), and may not be std or default — the generated rust crate's [features] table already owns those two keys, and this flag's key is written last, so a same-named value would silently overwrite one of them.
Default: wasm
cddl-codegen --input=example --output=export --rust-wasm-feature used_from_wasm
--binary-wrappersGenerates named Rust wrapper types for byte string definitions instead of treating aliases to bytes/bstr as the primitive byte type. Off by default.
Possible values: true, false
cddl-codegen --input=example --output=export --binary-wrappers true
--no-synthesized-rust-collection-aliasesSuppresses emission of Rust pub type aliases for generator-synthesized collection wrappers — currently a table rule's auto-named keys-list, e.g. pub type FooList = Vec<Foo>; minted for tbl = { * foo => uint }. Off by default.
Rule-declared aliases are never suppressed, even when structurally transparent: an explicitly authored foo_list = [* foo] or signature = bytes .size 32 is a human-written name and always stays. The suppression is emission-only — generated code references collections structurally (Vec<Foo>), never through the alias, so no field type or serialization changes, and the crate still compiles. Wasm-side wrappers and aliases are untouched.
This is a flag rather than a default change because it removes public API (the crate's own synthesized aliases, not just extern-element ones) that existing consumers may depend on.
Possible values: true, false
cddl-codegen --input=example --output=export --no-synthesized-rust-collection-aliases true
--preserve-encodingsPreserves CBOR encoding upon deserialization e.g. definite vs indefinite, map ordering. For each module this will also create a cbor_encodings.rs file to potentially store any structs for storing these encodings. This option is useful if you need to preserve the deserialized format for round-tripping (e.g. hashes) or if you want to modify the format to coincide with a specific tool for hashing.
Possible values: true, false
cddl-codegen --input=example --output=export --preserve-encodings true
--canonical-formRequires --preserve-encodings (the canonical toggle is emitted on the preserve-encodings serialize signatures; on its own the generated crate does not compile, so the combination is rejected). Provides a way to override the specific deserialization format and to instead output canonical CBOR. This will have Serialize's trait have an extra to_canonical_cbor_bytes() method. Likewise the wasm wrappers (with --to-from-bytes-methods) will contain one too.
Canonical form follows RFC 7049 §3.9 "Canonical CBOR": definite, minimal-length encodings, and map keys sorted length-first, then bytewise. Note this key ordering differs from RFC 8949 §4.2.1 "deterministic encoding" (pure bytewise-lexicographic) whenever a longer-encoded key is bytewise smaller — e.g. the uint key 256 sorts after the text key "a" under RFC 7049 but before it under RFC 8949.
For floats, "minimal-length" means the shortest float head (0xf9/0xfa/0xfb, RFC 8949 §3.3) that represents the value exactly — RFC 8949 §4.1 preferred serialization, unqualified. This is also what a NON-canonical write does for a value with no recorded width, so canonical form's only float effect is to DISCARD a recorded width: a float member read at 0xfb and holding 1.0 re-emits at 0xfb normally and canonicalizes to 0xf9 0x3c00, while 1.1 stays a double either way. A member's CDDL float name never enters this choice, because a value that name admits already has that width as its shortest form (see Floats). NaN is the one case where canonicalizing changes the VALUE and not just the head: any NaN, carrying any payload, at any width, canonicalizes to the zero-payload quiet NaN (RFC 8949 §4.2.2), written as 0xf9 0x7e00. A non-canonical write preserves the payload instead, so a payload-carrying NaN round-trips byte-exactly through to_cbor_bytes() and collapses only through to_canonical_cbor_bytes().
Possible values: true, false
cddl-codegen --input=example --output=export --preserve-encodings true --canonical-form true
--json-serde-derivesDerives serde::Serialize/serde::Deserialize for types to allow to/from JSON
Possible values: true, false
cddl-codegen --input=example --output=export --json-serde-derives true
--json-schema-exportTags types with JsonSchema derives and generates a crate (in wasm/json-gen) to export them.
Normally passed together with --json-serde-derives, because a schema describes the JSON surface those derives are what produce. The two flags are nevertheless independent — nothing implies one from the other and nothing rejects either combination. --json-schema-export alone generates types deriving schemars::JsonSchema and neither serde::Serialize nor serde::Deserialize, so the exported document describes a JSON surface the crate itself does not emit. The one place the pair is coupled is the generated rust/Cargo.toml, which declares serde_json under either flag: under this one the crate hosts the json_schema_gen module described below, whose reference-closure check walks a serde_json::Value.
The json-gen crate writes one document per crate, wasm/json-gen/schemas/<lib_name>.schema.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "<lib_name>",
"$defs": { "Foo": { }, "Bar": { } }
}
A pure definitions bundle — no top-level type/properties. Its $defs keys are schemars::JsonSchema::schema_name() values (for a generated type, the Rust type name; for an extern, whatever its hand-written impl returns), and its contents are byte-stable across regenerations of the same spec.
The two checks the run enforces — published-name injectivity and reference closure, both below — are not emitted into the json-gen crate. They live in the rust runtime crate as a json_schema_gen module, and the json-gen crate imports them (use <lib>::json_schema_gen::Registrar;, use <lib>::json_schema_gen::check_schema_ref_closure;). In-crate that module is rust/src/generated/json_schema_gen.rs; under --common-import-override it comes from the common crate, which --export-static-crate writes it into — so a workspace of N generated packages carries one copy rather than N. See --export-static-crate below for the pub mod json_schema_gen; a hand-owned common crate must declare, and for the recipe to put it behind a cargo feature.
The same module carries the two items your own code calls rather than the generated code: the custom_schema_impl! macro and the custom_schema_body function under it, which write the schemars::JsonSchema impl a hand-authored schema body needs — the impl @custom_json commits a spec author to, and the one a hand-written _CDDL_CODEGEN_EXTERN_TYPE_ needs before its registration row can compile. Nothing this tool emits calls either; the tool emits the ROW, and the impl has to exist for that row to build. #[macro_export] hoists the macro to the hosting crate's root (<lib>::custom_schema_impl!, never <lib>::json_schema_gen::custom_schema_impl!), and its expansion reaches back as $crate::json_schema_gen::…, so that crate needs json_schema_gen reachable from its root — in-crate that is the seed-once src/lib.rs's pub use generated::*;, which you own after the first export. The five placement rules, both macro forms and the by-hand fallback are in Comment DSL § Writing the JsonSchema impl the directive promises.
Two public functions build it. export_schemas() writes the document; add_schemas(&mut schemars::SchemaGenerator) registers this crate's types into a generator you own, and is the composition point for threading several crates' types into one document. --json-schema-dep below is the supported spelling of that composition — it emits the dependency's add_schemas call into this crate's, so you do not hand-write the wiring; add_schemas stays pub for the layouts the flag does not cover.
add_schemas emits one registration row per exported type. Each _CDDL_CODEGEN_EXTERN_TYPE_ / _CDDL_CODEGEN_RAW_BYTES_TYPE_ extern therefore must implement schemars::JsonSchema (part of the extern contract under these flags — see Comment DSL § _CDDL_CODEGEN_EXTERN_TYPE_). Two row classes are deliberately not emitted, as neither could compile here: the bare BASE of an extern generic (Foo in foo<T> — it names no concrete type; the concrete instances still get rows), and any type owned by a workspace dependency (an extern-deps-dir / --extern-import type) — in workspace mode each crate's own json-gen run exports its own schemas, and this crate's json-gen manifest depends only on the own rust crate.
A row is a declaration that a type is part of the published surface; it is not the list of definitions. $defs additionally holds every type reached from a row'd type, so a type with no row of its own is still declared whenever something published references it. Nothing downstream distinguishes the two — the emitted TypeScript declares every $defs entry either way.
Controlling what the document holds. The row set defaults to the spec, and four mechanisms adjust it. The first three change this crate's row set, and which one you want follows from what you are changing — a row, or a reference:
- Add a row for a published type your CDDL never describes (hand-written machinery whose JSON form is API, in this crate or in one with no spec at all):
--json-schema-rootbelow, which takes a Rust type path. - Drop a row for a rule whose derived schema is not that type's published encoding:
@no_json_schema_exporton the rule. It removes the declaration, so a type a published type still references stays in$defsand still reaches the emitted TypeScript. - Keep a type out of
$defsentirely — the only mechanism that can, because a reference that survives has to resolve:@custom_jsonon every rule that would reference it, which removes the derives and makes you supply the JSON impls by hand. This is also what an own-spec extern with noschemars::JsonSchemaimpl needs on its parents, on top of@no_json_schema_exporton itself.
The fourth is different in kind, and that difference is the point of choosing it:
- Import another crate's whole row set rather than adjusting this one's:
--json-schema-depbelow, which emits a dependency'sadd_schemascall at the top of this crate's. You do not name types at all — the dependency's own generation decides its row set, so the two never drift. Reach for it when a dependency's UNREFERENCED roots belong in your published surface: with one document per crate, everything your own types reference is already here through the closure, so a dependency's roots that nothing here points at are exactly what is missing. Covering them with--json-schema-rootinstead means hand-restating the dependency's root list in your build script — a duplicate that silently drifts from it.
The two directives and the two flags do not conflict: the flags consult no IR, so an extra root naming a @no_json_schema_export type re-registers it, and @custom_json + @no_json_schema_export on one rule is legal and means both things at once.
Published names must be injective, and the json-gen run enforces it. A $defs key is a published API name — run-json2ts.js suffixes it with JSON and json2ts emits that as the TypeScript type name — so add_schemas fails the run, naming both offenders, when any of these holds:
- two distinct Rust types publish one
schemars::JsonSchema::schema_name(), or - a registered type does not keep its own name in the document (another type claimed it first, so this one is published as
<name>2), or - a row whose type is inlined by
schemars(JsonSchema::inline_schema()is true, so the type contributes no definition of its own and the row publishes the returned body under its own name) finds that name already defined with a different body.
cddl-codegen --json-schema-export: two distinct Rust types both publish the JSON schema name
"ExtSet":
cddl_lib::ExtSet<u64>
cddl_lib::ExtSet<alloc::string::String>
…
cddl-codegen --json-schema-export: cddl_lib::generated::Shared publishes the JSON schema name
"Shared", but the document assigned it "Shared2" — another type claimed "Shared" first. …
cddl-codegen --json-schema-export: cddl_lib::Inlined publishes the inline JSON schema name
"Shared", but the document already defines "Shared" with a different body. …
All three are otherwise silent: schemars::JsonSchema::schema_id() defaults to schema_name(), so two types sharing a hand-written constant name are one type to schemars — one definition is emitted and every reference to the other resolves to its shape. When the ids do differ, both are emitted but which one gets the bare name (and which gets <name>2) follows first-encounter order, so an unrelated spec edit can silently rename a published TypeScript type. An inlined type contributes no definition at all, so its row would otherwise leave whichever body reached the name first in place — publishing one type's shape under the other's name with nothing said. A spec that ships such a name today will start failing its json-gen run: give one of the types a schema_name() unique within the crate (for a generic extern, vary it with the parameters — see Comment DSL § _CDDL_CODEGEN_EXTERN_TYPE_).
Two bounds are worth stating, because both are about what the checks cannot see rather than what they do.
Cross-crate collisions. The ledger belongs to the Registrar one crate's add_schemas opens, so it never sees two different crates' registrations. Under --json-schema-dep the tool now emits that composition itself, and it emits the dependency's call first — so the consumer's colliding row is the one that gets <name>2 from subschema_for and the kept-its-own-name check fires on it, naming the side whose owner can change it. What that leaves uncovered is the collision whose schema_ids match: there schemars sees one type, both returned refs equal the shared name, and only a ledger spanning both crates could see it. (The blame direction is measured by a two-crate vector, not merely inferred from the single-crate <name>2 check.)
Collisions where neither type has a row. Both checks are row-side — the ledger holds rows, and "kept its own name" is asked only of a registered type — so two types reached only through other types' schemas can collide invisibly. The common shape is a generic instantiated twice with a constant schema_name(), referenced from fields rather than published in its own right. You can make the guard see it: give each instantiation a row with --json-schema-root (--json-schema-root=my_crate::AssetBundle<u64>, …<i64>). Registering both turns it into two rows publishing one name with different core::any::type_names, which the ledger reports; registering just the one that lost the bare name makes the kept-its-own-name check report the <name>2 it was assigned. (Reasoned from the emitted helper and the flag's accepted charset — generic arguments are inside it — rather than from a fixture.)
The document must also resolve its own references, and the same run enforces that. Before anything is written, export_schemas() walks the finished document and fails — naming every offender, in a sorted list — when a $ref is not an internal pointer at one of that document's own definitions:
cddl-codegen --json-schema-export: the exported JSON schema document holds references that do not resolve inside it:
"#/$defs/RemovedFromTheDocument" — "RemovedFromTheDocument" is not defined in this document
"PlutusData" — not an internal "#/$defs/<key>" reference
…
The two classes are the two ways a hand-written schemars::JsonSchema impl goes wrong: returning a bare Schema::new_ref("SomeType") where a schema body belongs, and pointing at a key that no longer has a registration row. Anything that is not an internal reference fails, including an http(s):// URL and a bare "#" — the document is a self-contained bundle by contract, and run-json2ts.js compiles it in one pass with no external resolution, so such a reference ships as a .d.ts that declares a type it never defines (TS2304). Fix it by returning the real schema body, or by giving the referenced type a registration row of its own (a CDDL rule, or --json-schema-root below) so the document defines it — Comment DSL § Writing the JsonSchema impl the directive promises is the worked pattern for the hand-written impls @custom_json commits you to. The check runs before the write, so the failing document never reaches schemas/ — but note what that leaves behind: a run that fails it writes nothing at all, so an earlier export's document stays on disk untouched. It is now stale, and a pipeline that ignores the non-zero exit and proceeds to run-json2ts.js will happily compile that older document. Treat the failed run as the stop signal it is.
export_schemas() never deletes anything, so schemas/ may also hold files you own — including a *.json that is not the document. run-json2ts.js requires exactly one *.schema.json there and fails naming every other .json file it finds, rather than compiling a surface that may no longer match the spec.
Where the failures show up. Three of them are your own build's, not this tool's, so the message names its own cause rather than the flag that created it:
cddl-codegen --json-schema-export: two distinct Rust types both publish …/… but the document assigned it "<name>2" …/… publishes the inline JSON schema name …— a panic incargo runof yourwasm/json-gencrate. Two published names collided; see the injectivity section above.cddl-codegen --json-schema-export: the exported JSON schema document holds references that do not resolve inside it:— a panic in that same run, before the document is written. A hand-writtenJsonSchemaimpl returned a reference where a body belongs, or points at a type with no row; see the reference-closure section above.E0433/E0412inwasm/json-gen/src/generated/mod.rs, on a path you recognise from your command line — a--json-schema-rootvalue that does not resolve from the json-gen crate. The flag emits its value verbatim and this tool does not typecheck Rust, so reachability is checked by your compiler; see--json-schema-rootbelow.E0433in that same file onuse <crate>::json_schema_gen::…, naming your--common-import-overridevalue — the json-gen crate imports the row registrar and the closure check from the common runtime crate, and that dependency is yours to add towasm/json-gen/Cargo.toml(the tool cannot derive a cargo package name from an override that is a Rust path prefix; the merged manifest keeps your entry across regenerations). Without an override there is nothing to add — the module comes from the generated rust crate the json-gen manifest already depends on.E0433in that same file, naming a crate you recognise from your command line — a--json-schema-deplib name thatwasm/json-gen/Cargo.tomldoes not depend on. Declare it:--json-gen-dep=<cargo-package-name>=<path>writes the[dependencies]entry, and the merged manifest is what makes a hand-added one survive regeneration if you prefer to own it yourself. See--json-schema-depand--json-gen-depbelow.
The JS side fails separately and always non-zero, leaving the last-good json-types.d.ts in place — see --json-schema-scripts below for those.
The spec author can drop a further row with @no_json_schema_export on the rule. That is the escape hatch for the case the tool cannot decide: a type whose derived schema is not its published encoding (the real one comes from a parent's hand-written impl, or its JsonSchema impl is a deliberate stub), and — the hard blocker — an own-spec extern whose hand-written rust type has no schemars::JsonSchema impl, whose row would otherwise be an E0277 inside a generated file. It removes the row only: the derives, the CBOR/wasm surfaces, and the extern-interface export are untouched, and it is inert when this flag is off. Note that dropping the row is not on its own enough for an extern with no JsonSchema impl: a generated type embedding it still derives JsonSchema over that field, so those parents need @custom_json too — with one exempt embedding position, an open struct-map rest row's key domain, whose flattened-region schema is derived from the key's string image rather than from the key type. See the worked example under Comment DSL § @no_json_schema_export.
Possible values: true, false
Default: true
Example:
cddl-codegen --input=example --output=export --json-schema-export true
--package-jsonGenerates a npm package.json along with build scripts (some of these scripts require --json-serde-derives/--json-schema-export to work).
With --json-schema-export it also copies the two JSON-schema → TypeScript scripts into <output>/scripts/ (see --json-schema-scripts below for what they do, and for how to take them without the manifest).
Possible values: true, false
Default: false
cddl-codegen --input=example --output=export --package-json true --json-schema-export true
--json-schema-scriptsCopies the shipped JSON-schema → TypeScript scripts — run-json2ts.js and json-ts-types.js — into <output>/scripts/ without writing a package.json. --package-json --json-schema-export copies the same two scripts alongside its own manifest; this flag is the opt-in for a project that hand-maintains its npm manifests and wants the canonical scripts rather than a fork of them. Passing both flags copies once, and --package-json's behaviour is unchanged.
Requires --json-schema-export: the scripts compile the schema document the wasm/json-gen crate writes, so without it there is nothing to read (the combination is rejected up front).
What the scripts do, in order:
run-json2ts.jscompiles the singlewasm/json-gen/schemas/*.schema.jsondocument to TypeScript in one pass and writeswasm/json-gen/output/json-types.d.ts. Every definition becomes one top-level declaration named after its$defskey with aJSONsuffix (Foo→FooJSON), so the emitted names cannot collide with the wasm-bindgen classes they describe and no referenced type is left undeclared. The run fails (non-zero exit, the error printed in full) and leaves the previous output untouched if the document does not compile, if the schemas directory holds anything other than exactly one*.schema.json, or if the document has no definitions.json-ts-types.jsrewrites the wasm-pack.d.tsin place: it retypes each class'sto_json_value(): anyto that class's<Class>JSONtype and appends the emitted declarations under a marker line. A class that declares the JSON method but has no emitted JSON type fails the run (non-zero exit, every offender named, the.d.tsuntouched) — a silently-anymethod is the silent-publication mode this pipeline exists to close, and the fix belongs at the document: a CDDL rule,--json-schema-rootfor a hand-written type, or--json-schema-depfor a dependency whose classes ship in this package. A deliberate exception is spelled per class with--allow-untyped=<Class>[,<Class>...]; such a class keeps itsany(a name nothing declares would be aTS2304), and a stale exception — the class is typed now, or gone — is itself an error, so the list cannot rot. The appended block is truncated and rewritten on every run, so re-running without arimraf ./pkgis a fixed point rather than a duplicate-declaration error.
One legitimate configuration lands in that failure by design, and --allow-untyped — not a document change — is the answer for it. @no_json_schema_export removes a type's registration row, not its derives, so a suppressed rule that mints a generated wasm class still declares to_json_value(): any; if nothing published references that type either (the @custom_json-on-every-parent recipe above), it is absent from $defs and step 2 fails on it. The directive's own section states which rule shapes reach that state and which cannot.
Two documentation limits of that first step, both deliberate. A Rust doc comment reaches the schema as a title only when it opens with a markdown heading (a plain doc comment yields description alone), and the script overwrites every definition's title with the suffixed $defs key regardless, because json2ts names a type from its title in preference to its key — an unsuffixed name would merge with the wasm-bindgen class it describes. And annotations sitting beside a $ref are stripped, because json2ts reads {"$ref": …, "description": …} as a schema distinct from its target and emits a near-duplicate FooJSON1; the cost is that a field-level doc comment on a $ref-typed field does not reach the emitted TypeScript.
Both scripts locate everything relative to their own directory (<root>/scripts/*.js), so they work in either layout — the wasm crate at <root>/rust/wasm under --package-json, or at <root>/wasm without it — and do not depend on the caller's working directory. Each accepts --name=value overrides for a non-standard layout: --root=<dir> and --wasm-dir=<dir> on both, plus --dts=<path>, --method=<name> and --allow-untyped=<Class>[,<Class>...] on json-ts-types.js. An unrecognized argument is an error, never a silent fallback.
--dts exists because the wasm-pack .d.ts is named after the crate, so it tracks --lib-name; by default the script picks the single non-_bg .d.ts in pkg/. --method exists because under --wasm-cbor-json-api-macro the JSON methods are named by your macro body, not by cddl-codegen: if the script finds a class that has an emitted JSON type and still declares a zero-argument method returning any, but nothing it could specialize, it exits non-zero naming the method it looked for rather than leaving every method untyped.
Possible values: true, false
Default: false
cddl-codegen --input=example --output=export --json-schema-export true --json-schema-scripts true
--json-schema-rootRegisters an additional JSON-schema root: one extra reg.add::<RUST_PATH>(); row in the json-gen crate's add_schemas, for a type that is part of your published surface but that your CDDL never describes. The motivating case is hand-written machinery whose JSON form is API while its bytes are not a CDDL rule — a Byron address type, a key type — including one owned by a crate that has no spec and no json-gen crate of its own. rust_structs() cannot see such a type and no closure over the document can invent it, because nothing in the spec points at it.
The value is a Rust path, not a CDDL identifier, and it is emitted verbatim. So:
- The path must resolve from the json-gen crate.
cddl-codegendoes not typecheck Rust, so a path that does not resolve is anE0433/E0412in your own json-gen build — never a generation-time reject. Confirming reachability is yours, not the tool's. - Reaching another crate's type needs that crate in the generated
wasm/json-gen/Cargo.toml. Declare it with--json-gen-depbelow, or add it by hand — the manifest is merged, so keys the tool does not manage pass through untouched and a hand-added dependency survives regeneration either way. That is a manifest edit, not aninsertblock in generated code. Re-exporting the type through your own rust crate is the other route. - What the flag takes is a type path — a name, optionally with generic arguments — and not an arbitrary type expression.
cddl_lib::sub::Ext<u64, String>and<Foo as Trait>::Assocare expressible; an array ([u8; 32]), a tuple ((A, B)) and a reference (&T) are not. That boundary is forced rather than incidental: the value lands verbatim inside a turbofish in a generated file, so the accepted characters are exactly[A-Za-z0-9_],:,<,>,,and space — everything else could introduce a comment, a statement separator or a string literal into that file, and;(the separator array syntax needs) is the clearest case of a character the guard cannot admit. Anything else is rejected naming the offending character. Give such a type a named alias in your own crate and register that.
Repeatable. Extra roots are emitted after every spec-derived row, in flag order (never sorted): registration order decides which side of a published-name collision the injectivity guard names, and with the CLI roots last a spec-derived row keeps its own name and the guard blames the CLI-supplied path — the one you can change without touching your spec.
An extra root goes through the same registrar as every other row, so it is subject to the same published-name injectivity guard documented under --json-schema-export above (and, like any row, its type must implement schemars::JsonSchema).
Three further behaviours worth stating outright:
- Requires
--json-schema-export; without it there is no json-gen crate and noadd_schemasfor the row to land in, so the combination is rejected up front. - Passing the same value twice is a hard error naming the repeated value. This is exact-string dedup only: two spellings of one type (
crate::Fooandcddl_lib::Foo) are not detected, and are harmless — the guard's ledger keys on the Rust type, so registering one type twice is a no-op rather than a collision. - The flag consults no IR at all, so naming a type whose CDDL rule carries
@no_json_schema_exportre-registers it. That is the intended escape hatch direction, not a conflict.
cddl-codegen --input=cml_chain.cddl --output=export --lib-name=cml-chain \
--json-serde-derives=true --json-schema-export=true \
--json-schema-root=cml_chain::byron::ByronAddress \
--json-schema-root=cml_crypto::Bip32PublicKey
--json-schema-depThreads a dependency's whole row set into this crate's schema document: one
<DEP_JSON_GEN_LIB>::add_schemas(generator);
call emitted at the top of the json-gen crate's add_schemas. Each value is <dep>=<dep_json_gen_lib_name>, and the flag is repeatable.
What it buys, and why --json-schema-root is not a substitute: with one document per crate, everything your own types reference is already in your document through the closure. What is missing is the dependency's unreferenced roots — types a consumer of your package holds directly (an address, a transaction, a hash) that nothing in your spec points at, so no closure can reach them. You could name each with --json-schema-root, but that means maintaining a copy of the dependency's root list inside your build script, which silently drifts from the dependency's actual row set. A dep call imports the list instead of restating it.
The two sides answer to different rules.
- The left side is a label, used only for duplicate detection and error messages. It is never resolved against your extern-dep set, because the emitted line depends solely on the right side — a wrong label is inert, not wrong. Write the dependency's rust-crate name there so the line reads next to the
--extern-wasm-crate=<dep>=<dep>_wasmline for the same dependency; nothing enforces that. - The right side is a rust module path, emitted verbatim as a call path. Accepted characters are
[A-Za-z0-9_],:and-(normalised to_, so a cargo package name works as written);<,>,,and space are not — a module path has no generic arguments, and anything else could introduce a comment, a statement separator or a string literal into a generated file. Both a crate (cml_chain_json_schema_gen) and a path to a re-export (crate::vendored) are expressible.
This flag does not itself touch wasm/json-gen/Cargo.toml. It knows the crate name but not where the crate lives, so the [dependencies] entry comes from --json-gen-dep below — which carries the path — or from a hand edit, if you would rather keep that manifest entirely yours. It is merged, never clobbered, so either survives regeneration, and the two converge on one entry rather than fighting if you do both. Two more references have the same shape and the same choice: --json-schema-root's cross-crate roots, and — under --common-import-override — the json-gen crate's import of Registrar / check_schema_ref_closure from <override>::json_schema_gen (an override is a Rust path prefix, so the tool cannot derive a package name from it; name the package on --json-gen-dep yourself). A name the manifest does not depend on is an E0433 in your own json-gen build naming the crate, never a generation-time reject.
Dep calls come first. They are emitted before the name ledger, before every spec-derived row, and before every --json-schema-root row, in flag order (never sorted — the flag list is an input, and sorting would rewrite it). This is the deliberate mirror of why CLI roots come last: a dependency's published names are already shipped in the dependency's own package, so on a cross-crate name collision it is your row that should be renamed, and registering the dependency first is what makes the injectivity guard blame it (see the cross-crate bound under --json-schema-export above).
Regeneration order does not matter here; currency does. The emitted line is built from the flag value alone and reads nothing of the dependency, so there is no ordering obligation — regenerate the two crates in either order, including a consumer-before-dependency order some other flag forces on you. What there is instead is a completeness property: a consumer's document is only as current as its dependency's last regeneration. The row set arrives as compiled code, so a dependency whose json-gen crate is stale contributes its stale rows silently and the consumer's build is green either way. This is deliberately unlike --extern-import and --extern-wrapper-index, whose "regenerate the dependency first" discipline exists because there the consumer's pass reads a file the dependency's pass wrote.
Three further behaviours worth stating outright:
- Requires
--json-schema-export; without it there is no json-gen crate and noadd_schemasfor the call to land in, so the combination is rejected up front. - Passing the same label twice is a hard error naming it: a dependency has one json-gen crate, so two mappings for one label are ambiguous rather than additive.
- Passing one lib name under two labels is a hard error naming it:
add_schemasregisters a fixed set into the generator it is handed, so the second call registers nothing the first did not. Both checks compare verbatim strings (after trimming, and after dash normalisation for the lib name), so two spellings of one registrar are not detected — and are harmless for that same reason.
cddl-codegen --input=cml_multi_era.cddl --output=export --lib-name=cml-multi-era \
--json-serde-derives=true --json-schema-export=true \
--extern-wasm-crate=cml_chain=cml_chain_wasm \
--json-schema-dep=cml_chain=cml_chain_json_schema_gen \
--json-gen-dep=cml-chain-json-schema-gen=../../../chain/wasm/json-gen
In a config file you do not write this flag at all for a dependency the config contains. Both halves — this call and the --json-gen-dep entry that lets it link — are derived from the crate's deps and wasm-reexports lists, in that order, so a forgotten thread stops being possible. See Config file § The published JSON surface.
--json-gen-depDeclares a [dependencies] entry in the generated wasm/json-gen/Cargo.toml:
<cargo-package-name> = { path = "<path>" }
This is the manifest half of every cross-crate reference the json-gen crate can make. Three flags produce such a reference and none of them can write the entry, because a Rust path names a crate but never says where it lives:
--json-schema-depabove — the dependency's registrar call.--json-schema-rootabove, with a path rooted in another crate (cml_crypto::Bip32PublicKey).--common-import-overridebelow — theRegistrar/check_schema_ref_closureimport.
Pass this flag once per crate they need. Without it the name is an E0433 in your own json-gen build; with it, the build links.
The left side is the cargo package name — the opposite spelling from --json-schema-dep's right side. A manifest key is dashed (cml-chain-json-schema-gen); a Rust path is underscored (cml_chain_json_schema_gen). Writing them the same way is the obvious mistake, and neither resulting error points at it: a package name cargo cannot find is a resolution failure, and a missing one is an E0433 on a crate whose name looks right. The accepted charset is cargo's own, [A-Za-z0-9_-]; anything else is rejected naming the character.
The right side is a path, written verbatim, and relative to the manifest. That is what a cargo path dependency always means, so a relative value counts from <output>/wasm/json-gen/. The entry the tool already writes for your own rust crate — <lib-name> = { path = "../../rust" } — is the shape to count from; a sibling generated crate is typically ../../../<sibling>/wasm/json-gen. Absolute paths work too. The path takes any characters (it is quoted and escaped by the TOML writer, not spliced into it), and the tool does not check that it exists — an unresolvable one is a cargo error naming it.
The entry is asserted, never removed. It merges field-level into whatever entry is already there, so an entry you added by hand for the same package converges rather than duplicating: this flag's path wins, and your version, optional, features and comments survive. The other direction is the part to know before you rely on it — dropping the flag leaves the entry behind. Unlike the conditional deps the tool removes when their flag goes away, the package name here lives only inside the flag value, so a run without the flag has no name to tombstone. Remove a dependency you no longer want by hand, as you would in any manifest. (This is also what makes the flag safe: it can never delete a dependency you added yourself.)
Repeatable. Passing the same package name twice is a hard error naming it: a manifest holds one [dependencies] entry per package, so a second path would silently replace the first rather than adding anything. Two different packages may share one path.
Requires --json-schema-export; without it there is no json-gen crate and no manifest for the entry to land in, so the combination is rejected up front.
cddl-codegen --input=cml_multi_era.cddl --output=export --lib-name=cml-multi-era \
--json-serde-derives=true --json-schema-export=true \
--json-schema-dep=cml_chain=cml_chain_json_schema_gen \
--json-gen-dep=cml-chain-json-schema-gen=../../../chain/wasm/json-gen
In a config file this is derived, for a dependency the config contains, alongside the --json-schema-dep call it exists to support — including the relative path, counted from the consumer's json-gen directory to the dependency's and following each crate's own package-json layout. See Config file § The published JSON surface. Writing the sub-table by hand stays the way to name a crate the config does not contain.
--wasm-dep below is the same move on the other manifest — same value shape, same merge contract, same asserted-never-removed consequence — for the cross-crate names the wasm crate emits rather than the json-gen crate's.
--wasm-depDeclares a [dependencies] entry in the generated wasm/Cargo.toml:
<cargo-package-name> = { path = "<path>" }
The same move as --json-gen-dep above, for the other manifest: the manifest half of a cross-crate reference, which the reference itself can never carry because a Rust path names a crate but never says where it lives.
What produces such a reference here is an extern dependency (--extern-import, with --extern-wasm-crate / --extern-wrapper-index). The wasm pass writes two kinds of reference to a dependency's type, so a consumed dependency generally needs both of its packages declared:
use <dep>_wasm::…— the wasm boundary, and any wrapper class borrowed from the dependency's index. That is the dependency's wasm package.BTreeMap<own::Key, <dep>::Value>and friends — the inner storage of a wrapper this crate mints itself (a mixed-dep collection is minted here, since no single dependency can host it). That is the dependency's rust package.
Pass the flag once per package. Without it the name is an E0432/E0433 in your own wasm build; with it, the build links. A dependency that generates no wasm crate of its own keeps its rust crate name for both passes, so that edge is one entry rather than two.
The left side is the cargo package name — the opposite spelling from --extern-wasm-crate's right side. A manifest key is dashed (cml-chain-wasm); a Rust crate name is underscored (cml_chain_wasm). Writing them the same way is the obvious mistake, and neither resulting error points at it: a package name cargo cannot find is a resolution failure, and a missing one is an E0433 on a crate whose name looks right. The accepted charset is cargo's own, [A-Za-z0-9_-]; anything else is rejected naming the character.
The right side is a path, written verbatim, and relative to the manifest. That is what a cargo path dependency always means, so a relative value counts from <output>/wasm/. The entry the tool already writes for your own rust crate — <lib-name> = { path = "../rust" } — is the shape to count from; a sibling generated crate is typically ../../<sibling>/wasm. Absolute paths work too. The path takes any characters (it is quoted and escaped by the TOML writer, not spliced into it), and the tool does not check that it exists — an unresolvable one is a cargo error naming it.
The entry is asserted, never removed, on exactly --json-gen-dep's terms and for the same forced reason. It merges field-level into whatever entry is already there, so an entry you added by hand for the same package converges rather than duplicating: this flag's path wins, and your version, optional, features and comments survive. Dropping the flag leaves the entry behind — the package name lives only inside the flag value, so a run without it has no name to tombstone. Remove a dependency you no longer want by hand. (This is also what makes the flag safe: it can never delete a dependency you added yourself.)
Repeatable. Passing the same package name twice is a hard error naming it. Requires --wasm=true; without it there is no wasm crate and no manifest for the entry to land in, so the combination is rejected up front.
cddl-codegen --input=cml_multi_era.cddl --output=export --lib-name=cml-multi-era \
--extern-import=cml_chain=../chain/extern-interface/cml_chain \
--extern-wasm-crate=cml_chain=cml_chain_wasm \
--wasm-dep=cml-chain=../../chain/rust \
--wasm-dep=cml-chain-wasm=../../chain/wasm
In a config file this is derived, from both edge keys and for different reasons: a deps edge contributes both of the dependency's packages (the two reference kinds above), while a wasm-reexports edge contributes the wasm one alone — nothing generated names that crate at all, and the entry exists so the npm build ships its classes. Paths are counted from the consumer's wasm directory to the dependency's, following each crate's own package-json layout. See Config file § The wasm manifest. Writing the sub-table by hand stays the way to name a crate the config does not contain.
--rust-depDeclares a [dependencies] entry in the generated rust/Cargo.toml:
<cargo-package-name> = { path = "<path>" }
The third of the three sibling flags — --json-gen-dep, --wasm-dep and this one — and the one on the manifest every run writes. Same value shape, same merge contract, same asserted-never-removed consequence.
What produces a cross-crate reference here is --extern-import: an imported dependency's types are emitted into your rust source as use <dep>::<Type>;. That happens in every flavor, --wasm or not, so a consumed dependency needs its rust package declared here — and it is the only package the rust pass can name, so this flag contributes one entry per dependency rather than two. Without it the name is an E0432 in your own rust build, and (because the wasm crate path-depends on the rust one) in every crate downstream of it.
The left side is the cargo package name — the opposite spelling from --extern-import's left side. A manifest key is dashed (cml-chain); the extern-deps directory name and the rust crate name are underscored (cml_chain). The accepted charset is cargo's own, [A-Za-z0-9_-]; anything else is rejected naming the character.
The right side is a path, written verbatim, and relative to the manifest, i.e. counted from <output>/rust/. A sibling generated crate is typically ../../<sibling>/rust. Absolute paths work too; the tool does not check that the path exists — an unresolvable one is a cargo error naming it.
The entry is asserted, never removed, on exactly its siblings' terms and for the same forced reason: the package name lives only inside the flag value, so a run without the flag has no name to tombstone. It merges field-level into a hand-added entry for the same package, so the two converge rather than duplicating.
Repeatable. Passing the same package name twice is a hard error naming it. Requires no other flag — the rust crate is the one crate every run generates, so there is no flavor in which its manifest goes unwritten.
cddl-codegen --input=cml_multi_era.cddl --output=export --lib-name=cml-multi-era \
--extern-import=cml_chain=../chain/extern-interface/cml_chain \
--rust-dep=cml-chain=../../chain/rust
In a config file this is derived from deps alone. wasm-reexports contributes nothing here, and the asymmetry is that key's meaning rather than an omission: it says a dependency's wasm classes ship in this crate's package while this crate's spec references none of its types, so no rust line names the crate. See Config file § The rust manifest. Writing the sub-table by hand stays the way to name a crate the config does not contain.
--component-depDeclares a [dependencies] entry in the generated component/Cargo.toml:
<cargo-package-name> = { path = "<path>" }
The fourth of the sibling flags — --json-gen-dep, --wasm-dep, --rust-dep and this one — on the component crate's manifest. Same value shape, same merge contract, same asserted-never-removed consequence.
The left side is the cargo package name, dashed (cml-chain-component), not the underscored Rust crate name a use line takes. The accepted charset is cargo's own, [A-Za-z0-9_-]; anything else is rejected naming the character.
The right side is a path, written verbatim, and relative to the manifest, i.e. counted from <output>/component/. The entry the tool already writes for your own rust crate — <lib-name> = { path = "../rust" } — is the shape to count from. Absolute paths work too; the tool does not check that the path exists, so an unresolvable one is a cargo error naming it.
The entry is asserted, never removed, on exactly its siblings' terms and for the same forced reason: the package name lives only inside the flag value, so a run without the flag has no name to tombstone. It merges field-level into a hand-added entry for the same package, so the two converge rather than duplicating.
Repeatable. Passing the same package name twice is a hard error naming it. Requires --component=true; without it there is no component crate and no manifest for the entry to land in, so the combination is rejected up front.
cddl-codegen --input=example --output=export --component=true \
--component-dep=cml-chain-component=../../chain/component
In a config file this is derived from a deps edge whose two crates both have component = true — and what it names there is the dependency's rust package, not its component package: the guest glue holds a dependency-typed value natively, while the dependency's own component crate is wired by the composer rather than by cargo. See Config file § The component face. Writing the sub-table by hand stays the way to declare anything else the component crate needs.
--component-extern-witOpts one dependency into import mode on the component face: its types cross as imported WIT resources instead of having no WIT projection at all.
cddl-codegen --input=example --output=export --component=true \
--extern-import=cml_chain=../chain/extern-interface/cml_chain \
--component-extern-wit=cml_chain=../chain/component/wit
The left side is the extern-deps directory name — the same value --extern-import takes on its left (cml_chain, underscored), not a cargo package name. The right side is the dependency's component/wit directory, i.e. <dep output>/component/wit, which is a different tree from the extern-interface/<dep> one --extern-import points at. The two flags answer two questions: the export puts the dependency's types in your spec's namespace, and the WIT says how they cross the component boundary. Each <dep> here must therefore also be declared — by --extern-import or by a physical _CDDL_CODEGEN_EXTERN_DEPS_DIR_/<dep>/ stub — or the run is rejected naming the missing declaration. Requires --component=true.
Why it is opt-in. Without this flag a dependency's types have no WIT projection, so any signature naming one is recorded as // unexported: in the emitted WIT. That is the right fallback rather than a failure: a dependency with no component face (a hand-written crate, a stub tree, a crate you cannot regenerate) has no component/wit to point at, and a mandatory flag would make such a spec ungeneratable under --component. Sharing is the payoff, so sharing is what you opt into.
What it produces. The dependency's WIT is copied, never re-derived — only its own run knows its --wit-package id and the reasons behind its // unexported: rows — into <output>/component/wit/deps/<dep>/, which is tool-owned and delete-and-recreated each run. Your interfaces then use cddl:chain/types@0.1.0.{token, …}, and the guest crate's wit_bindgen::generate! carries one co-required with: row per imported interface (without it the macro panics at your build, naming a key nobody wrote). The keys are read out of the copied WIT rather than reconstructed, because wit_bindgen matches them against that file.
Dependency-typed values cross through a CBOR bytes seam, so every door touching one is fallible: a getter on a dependency-typed field returns result<foo, string>, and so does a setter or constructor taking one. That is not defensive — the far side's from-cbor-bytes can genuinely fail on a value your own serializer produced when the dependency component and the dependency crate you link disagree about a type's shape, a failure class that is compile-time within one crate. The cost is one serialize + copy + deserialize per dependency-typed value per crossing.
A dependency type in a collection parameter (list element, map key, map value) is spelled as an accumulator resource — borrow<<dep-type>-list>, which the caller fills one element at a time — rather than as list<borrow<t>>, which is legal WIT that wit-bindgen's Rust backend cannot lower (E0506). Collection returns are unaffected and keep list<own t>.
One shape is rejected at generation rather than emitted: a signature naming a dependency type the dependency's own WIT excluded. The error quotes the dependency's recorded reason verbatim and names the dependency, because the fix is on that side.
Determinism class. The dependency's committed WIT is an explicit cross-crate input — the same class as --extern-import reading extern-interface/<dep>/** — and never a read of this run's own prior output; the copy carries a file-class comment saying so. As with --extern-import, regenerate the dependency before the consumer so the WIT it reads is fresh.
Repeatable.
In a config file this is derived from a deps edge whose two crates both have component = true, together with the --component-dep naming the dependency's rust package. A config also refuses such an edge when the two crates' encoding posture differs, which a hand-written invocation cannot check. See Config file § The component face.
--std-forward-depMarks a --rust-dep package as std-forwarding. Two things change in the generated rust/Cargo.toml:
[dependencies]
cml-core = { path = "../../core/rust", default-features = false }
[features]
std = ["cml-core/std"]
What it fixes, and why it is silent without. default-features = false on a generated crate turns that crate's std off. Its path dependencies keep their own defaults, so their std stays on and the #[cfg(not(feature = "std"))] arms inside them are unreachable from any downstream configuration — no error, no warning, the opt-in simply does nothing beyond the first crate. Forwarding is what makes the switch travel: the dependency is taken with its defaults off, and this crate's std turns it back on for a std consumer.
Only path dependencies need naming. The tool's own third-party dependencies forward automatically wherever they can — serde, serde_json, schemars and hex, the ones it ships in alloc mode and which have a std feature to name. (hex is a RENAMED dependency: the [dependencies] key is hex, the package it takes is const-hex. A forward names the key, which is what cargo resolves it against.) hashlink and cbor_event have none, so nothing forwards to them. See Output format § The std feature for the computed list and its merge rules.
The value is the cargo package name (cml-core) — the same dashed spelling --rust-dep takes on its left side, and the name that becomes the [dependencies] key. It must name a package this run also passes --rust-dep for: a default-features = false fragment with no path or version, and a feature naming an absent dependency, are both manifests cargo rejects, so a --std-forward-dep without its --rust-dep is a hard error naming both flags rather than a broken manifest the tool would be the author of. The target must also actually have a std feature — every crate this tool generates does; a package that does not is a cargo error naming the missing feature.
Repeatable; a repeated package name is a no-op rather than an error (unlike the <package>=<path> flags, a second occurrence carries no second value that could silently replace the first).
cddl-codegen --input=cml_multi_era.cddl --output=export --lib-name=cml-multi-era \
--extern-import=cml_chain=../chain/extern-interface/cml_chain \
--rust-dep=cml-chain=../../chain/rust \
--std-forward-dep=cml-chain
In a config file this is derived, twice over: once per deps edge, beside that edge's --rust-dep, and once per crate for the shared runtime crate when [runtime].lib-name names it. See Config file § The rust manifest.
--common-import-overrideOverrides the location of the static exports (e.g. error.rs, serialization.rs, etc).
This is particularly useful for combining multiple crates each generated using cddl-codegen where they all share a shared core directory where the static files are located.
Runtime-flavor contract: a crate generated without --preserve-encodings can target a preserve-flavored common crate (like cml_core). The generated deserializers construct CBORReadLen through From<cbor_event::Len>, which every crate exported by cddl-codegen (and cml_core) provides, so the cbor_event::Len a non-preserve crate reads is accepted by a preserve-flavored CBORReadLen. The mirror direction is not supported: a crate generated with --preserve-encodings cannot target a non-preserve-flavored common crate (there is no From<cbor_event::LenSz>).
Int and IntError route through the override. Int is common scaffolding parameterized only by the CLI flags (plus key demand) — the same class as error/serialization/ordered_hash_map — so under the override a spec referencing int emits pub use <override>::{Int, IntError}; rather than minting a crate-local enum. One shared Int identity per workspace is a hard constraint, not a preference: a forked rust Int fails E0308 at every crate boundary, and a forked wasm #[wasm_bindgen] Int cannot link into a single cdylib (duplicate symbol: __wbg_int_free). The re-export (rather than nothing) keeps unqualified Int references and pub type … = Int; aliases resolving, while the enum, its impls, and its ser/deser all live once in the common crate.
The wasm face. Under --wasm the wasm re-export is pub use <wasm-face>::Int;, where <wasm-face> is the --extern-wasm-crate mapping for the override key (the documented --common-import-override=cml_core --extern-wasm-crate=cml_core=cml_core_wasm pairing), falling back to the override string verbatim when unmapped — the single-crate convention where the override crate's rust types are themselves #[wasm_bindgen]-annotated. Int's wasm face must be a #[wasm_bindgen] class, so it has to come from the wasm crate; the bare override names the rust runtime crate, whose enum a wasm method returning Int cannot expose. The local pub type IntError = JsError; parity alias stays emitted regardless: the common wasm crate has no IntError — it is genuinely not a shared type (the wasm from_str maps rust's FromStr-associated IntError onto JsError).
Existence contract (deliberate non-feature). The tool does not verify the common crate actually exposes Int/IntError. If it does not, the pub use dangles and the consumer build fails with an unresolved import naming the common crate — a clear, actionable error. Remedies: reference int in the common crate's own spec, regenerate the common crate with a current tool version (an older one may predate IntError), or — for the key case — let a --key-requests row force emission (below).
Key-flavor across crates. Generated Int is key-flavor dependent: with no key demand it derives only Clone/Debug, but a map keyed on int needs the full Eq/Ord/PartialOrd bundle (plus Hash under --preserve-encodings, with encoding-insensitive comparisons). A consumer keying a map on int under the override therefore depends on the common crate's Int being key-flavored. That demand travels the existing rust/src/generated/borrowed_key_types.rs + --key-requests channel (see --workspace-dep and --key-requests below): the consumer records the row (<override>, "int"[, flavor]) iff the override also names a configured --workspace-dep, and the common crate's regen with --key-requests then emits key-flavored Int even if its own spec never references int (demand alone forces emission). When the override is not among the --workspace-deps — or is a path-form override like crate::common, which is not a bare crate name — no row is recorded and no error is raised at generation time; the consumer's own map sites fail E0277 naming Int instead, and the remedy is to declare the common crate as a --workspace-dep.
Default: unset. Unset, the runtime is emitted in-crate and reached as crate::generated from the rust crate and as the generated rust crate's own package name from the wasm and json-gen crates. Set, the value is used verbatim in both places — it is a Rust path prefix, not a cargo package name, so crate::common is as valid as a bare crate name (a bare crate name is the shape the workspace flags below are built around).
cddl-codegen --input=example --output=export --common-import-override=cml_core
--export-static-crateAdditionally exports the composed rust static runtime into the crate at <dir> (created if needed), independent of whether in-crate static export happens: the runtime files — error.rs, the serialization.rs prelude (the static runtime only, no generated per-type impls), ordered_hash_map.rs, non_empty.rs, non_empty_map.rs, ordered_set.rs (the @duplicates reject OrderedSet/NonEmptyOrderedSet twins), pair_map.rs (the @duplicates preserve PairMap/NonEmptyPairMap twins), and — under --json-serde-derives — json_value_ser.rs (the honest serde_json::Value walk a hand-written Serialize needs; see Wasm differences) — and — under --json-schema-export — json_schema_gen.rs (the json-gen helper module, below) — and — under either json flag — open_struct_rest_json.rs (the open struct-map rest-row JSON module: the #[serde(flatten)] mechanics under --json-serde-derives, the rest-row schema helper under --json-schema-export) — are written to <dir>/src/, and <dir>/Cargo.toml gets the static-runtime dependency changeset merged in.
This is the supported upgrade path for --common-import-override users. Override users own their copy of the static runtime in a shared crate, so cddl-codegen otherwise writes no static files for them — and that copy silently rots when the emitter and the runtime change together (e.g. a widening of the deserialization range-check type). Point this flag at the shared crate and regenerate: the freshly composed runtime is written, and the comment/code-preservation overlay carries any hand additions in those files forward (unless --no-preserve-comments).
The manifest merges, it is never clobbered — and it cannot be skipped. The exported source and the manifest that satisfies its dependencies are one artifact: the merge asserts exactly the dependencies the exported files reference (cbor_event and hex — the latter a RENAMED dependency, key hex, package const-hex — always; hashlink under --preserve-encodings, serde under --json-serde-derives, serde_json under --json-serde-derives or --json-schema-export, schemars under --json-schema-export), bumping a stale pin that no longer satisfies the required version while keeping a satisfying hand pin, extra features, and every key the changeset doesn't mention (same contract as the generated crates' manifests). It also asserts two [features] keys — "std" merged into default (union, your entries first) and a computed std list — because the exported ordered_hash_map.rs selects its hash builder on feature = "std". That feature is a REAL switch here, not an in-crate one: the deps above are asserted in alloc mode (default-features = false plus the explicit features the exported source needs, exactly the specs the generated rust crate gets) and the std list forwards to each of them that has a std feature — hex/std always, plus serde/std / serde_json/std / schemars/std on their own conditions. (hashlink and cbor_event declare no std feature, so nothing forwards to them.) This crate is the one the generated crates forward INTO, so a runtime whose deps stayed std-on would absorb every consumer's default-features = false one crate short of the dependency that matters. features.std merges as a union like default, with one addition: a <pkg>/<feat> entry whose <pkg> is not a [dependencies] key is pruned — which on this co-owned manifest can only ever drop a forward to a dependency you removed, since the tool removes none. Because this crate is co-owned with your hand code, the tool never removes a dependency from it (unlike the generated crates' set-or-remove rule) and only seeds package identity into a fresh manifest — an existing name/version/edition is never touched.
(This flag replaced --export-static-dir, which pointed at the src directory itself and left the manifest untouched — allowing exported source targeting a new cbor_event to land beside a manifest still pinning the old one. Migrate by pointing the new flag one level up, at the crate root.)
Pure function of the flag set. Unlike the in-crate static export (which gates non_empty.rs / non_empty_map.rs on [+ …] / {+ … => …} spec usage, raw_bytes_encoding on .cbor/.bytes usage, and the any_cbor module on CDDL any usage), the exported files and manifest changeset are a pure function of the flags, never of the spec that happened to be run — a shared runtime crate serves many specs, so which spec was run must not change the output. Therefore non_empty.rs, non_empty_map.rs, any_cbor.rs (the AnyCbor runtime lowering CDDL any, in its flag-appropriate mode flavor — preserve/canonical variants under those flags, the plain structural variant otherwise), and the raw_bytes_encoding prelude are always written. Flavor selection is otherwise identical to the in-crate composer: ordered_hash_map.rs appears only under --preserve-encodings; the serialization.rs prelude picks the preserve / canonical branches and appends the depth-guard runtime under --deserialize-depth-limit; the json / schemars companions append under --json-serde-derives / --json-schema-export; json_value_ser.rs appears only under --json-serde-derives and json_schema_gen.rs only under --json-schema-export; open_struct_rest_json.rs appears under either json flag and is itself two independently-gated halves — the serde flatten mechanics under --json-serde-derives, the rest-row schema helper under --json-schema-export — so a schema-only crate gets the helper alone; and non_empty_map.rs uses OrderedHashMap (not BTreeMap) under --preserve-encodings.
Which crates one exported runtime can serve. The exported content is a pure function of the flags (above), but whether another crate can compile against it is not symmetric across those flags, so a workspace sharing one runtime has to pick the exporting flag set deliberately. Two groups behave differently:
--preserve-encodings,--canonical-formand--deserialize-depth-limitmust MATCH. They are not "more is better". The canonical and non-canonical preludes givefit_sz,LenEncoding::to_len_szandSerializeEmbeddedGroupdifferent arities and putSerializein different crates, so a runtime exported at one value fails to build a crate generated at the other in both directions (E0061,E0405).--preserve-encodingslooks one-way — the preludes carry deliberateFrom<cbor_event::Len> for CBORReadLenaccommodations so a preserve runtime can serve a non-preserve crate — but that accommodation is incomplete today: a non-preserve crate whose spec holds a{+ K => V}buildsNonEmptyMapfrom aBTreeMapwhile the preserve runtime's is backed byOrderedHashMap(E0277), and under--canonical-forma non-preserve crate using CDDLanyfinds no one-argumentAnyCbor::serialize(E0599). And--deserialize-depth-limitbakes its value into the exportedAnyCborrecursion guard, so a mismatch compiles cleanly while guarding one crate'sanyvalues at the exporting crate's limit — a silent change to which documents are accepted.--json-serde-derivesand--json-schema-exportgenuinely nest. Their companions are appended to the runtime types, so a runtime carrying them serves a crate that does not; the reverse leaves that crate'sserde/schemarsimpls unresolved.
Placing this flag by hand means checking the exporting crate against those rules yourself. The config file's [runtime] table derives the choice instead, and refuses a config no single runtime can serve.
Exactly one invocation may export into a given <dir>. Two invocations at differing flavors pointed at one directory do not overwrite each other — they accumulate, and the run stops being idempotent. The exported crate sits outside the output crate and so outside the stale-file bookkeeping, leaving the other flavor's files in place; the manifest merge asserts dependencies and never removes one, so it accumulates the union of both flavors' deps; and the comment-preservation overlay reads the other flavor's file, cannot classify it, and injects a fresh compile_error! block every run. Measured on two specs differing only in --preserve-encodings: the exported any_cbor.rs grew 62 → 143 → 224 → 305 compile_error! blocks over four runs of an unchanged pair of commands (103 K → 509 K bytes), exit 0 each time. Nothing in a hand-written invocation can see the other one, so this is yours to keep straight — the config file can see both sites and refuses the shape before anything is written.
No mod.rs / lib.rs is written — the target crate owns its module declarations. The static files reference siblings via super::…, so a flat module directory works. Because that root is hand-owned, a runtime file that did not previously exist (every file on a first export; an added file like ordered_set.rs on a version bump) prints a stderr notice naming the pub mod <module>; line to add — without it the module sits dead in-tree and generated code fails to resolve it — E0432 where it is imported (use …::ordered_set::…), E0433 where it is reached by inline path (…::open_struct_rest_json::…) — cascading into spurious E0119 errors (see the migration notes in current_capacities). Idempotent re-exports print nothing.
json_schema_gen.rs, and how to put it behind a cargo feature. Under --json-schema-export the exported set gains json_schema_gen.rs: the row Registrar (which owns the ledger carrying the published-name injectivity guard), the add_schema helper it delegates to, check_schema_ref_closure (the document's reference-closure check), and — for your own hand-written code rather than for anything generated — the custom_schema_impl! macro and custom_schema_body under it. Every generated wasm/json-gen crate in the workspace uses the first three from here — use <override>::json_schema_gen::Registrar; — instead of carrying its own emitted copy, so N json-gen crates share one implementation. Like every other exported file it needs a hand pub mod json_schema_gen; in the target crate root the first time it appears (the notice above names it), and the merge asserts the schemars + serde_json it references.
That hand pub mod line is load-bearing for the macro specifically, and in a way the notice does not spell out: #[macro_export] publishes the macro at this crate's root as <override>::custom_schema_impl! whatever you do, but its expansion names $crate::json_schema_gen::custom_schema_body — so a target crate that declares the file under some other module name, or not at all, gets an E0433 at every invocation rather than at the declaration. The invocations themselves live in whichever crate DEFINES each type (the orphan rule leaves nowhere else), which for a generated type is a hand-owned module of the generated rust crate; see Comment DSL § Writing the JsonSchema impl the directive promises.
The tool asserts no cargo feature around it, deliberately: it never sees your crate root, so it cannot verify that a feature it named has a matching #[cfg], and schemars is in any case already a hard dependency of a --json-schema-export crate (every generated type derives schemars::JsonSchema unconditionally). Everything needed to gate it is hand-owned, and the tool will not fight you over any of it:
# <static-crate>/Cargo.toml — the tool asserts deps but never removes one, and the dep merge
# preserves fields it does not set, so `optional = true` survives every regeneration.
[dependencies]
schemars = { version = "1.2.1", optional = true }
serde_json = { version = "1.0.57", features = ["float_roundtrip"], optional = true }
[features]
json-schema = ["dep:schemars", "dep:serde_json"]
// <static-crate>/src/lib.rs — hand-owned; the tool writes no mod.rs/lib.rs here.
#[cfg(feature = "json-schema")]
pub mod json_schema_gen;
Three consequences to plan for. The generated wasm/json-gen crate's use of the module is not #[cfg]'d, so that crate must be built with the feature enabled (its Cargo.toml is merged, never clobbered, so a features = ["json-schema"] you add to its path dependency on the common crate survives regeneration). A generated crate that derives schemars::JsonSchema on its own types needs schemars unconditionally regardless — the feature gates this module, not the derives. And custom_schema_impl! is gated along with it: #[macro_export] publishes a macro only when the item defining it is compiled, so with the feature off every invocation is an E0433 for custom_schema_impl in the common crate. Gate your own invocations with the same feature.
cddl-codegen --input=example --output=export --common-import-override=cml_core \
--export-static-crate=../cml-core/rust
--extern-wasm-crateMaps a cross-crate extern dependency (declared under _CDDL_CODEGEN_EXTERN_DEPS_DIR_/<dep>/…) to the crate that holds its wasm-bindgen wrappers. Repeatable; each value is <dep>=<wasm_crate> (a malformed value, or a key that is neither a declared extern dependency nor the --common-import-override crate, aborts generation).
An accepted key is either a declared extern dependency or the --common-import-override crate — the latter routes the built-in Int's wasm face (below), and a pure --common-import-override consumer that declares no extern dependency of its own may name only that crate.
Use it when a dependency's wasm bindings live in a separate crate — the split <dep> / <dep>-wasm layout cddl-codegen itself generates (e.g. cml-core / cml-core-wasm). In the wasm pass, the use import and the wasm-boundary type path for that dep's types (as a direct member, and as elements of generated list/map wrappers) are qualified through the wasm crate, while the wrapper's inner storage stays the dep's rust type. Without a mapping the dep keeps its rust crate name for both passes — the single-crate convention, where the dependency's rust types are themselves #[wasm_bindgen]-annotated, which continues to work with no configuration.
The mapped wasm crate must mirror the rust crate's module tree (as cddl-codegen-generated wasm crates do: thin root + glob re-export), so dep::sub::Foo resolves to dep_wasm::sub::Foo. It must also provide, for each boundary type, From<native> for wrapper / From<wrapper> for native (the generated get/add/insert bodies convert with .into(), cloning first unless the type is a declared-@copy extern crossing rust→wasm).
The generated wasm Cargo.toml then needs both dep crates as dependencies (the rust crate for inner storage, the wasm crate for the boundary). Declare them with --wasm-dep above, or add them by hand — the manifest is merged, so keys the tool does not manage pass through untouched and a hand-added dependency survives regeneration either way. The consumer's rust/Cargo.toml needs the dep's rust crate too, for the use <dep>::<Type>; lines the rust pass emits; that entry is --rust-dep below.
The mapping keyed on a --common-import-override value also selects the wasm face of the common Int (pub use <wasm-face>::Int;), falling back to the override crate name when unmapped — see --common-import-override above.
cddl-codegen --input=example --output=export --common-import-override=cml_core --extern-wasm-crate=cml_core=cml_core_wasm
--extern-wrapper-indexPoints the consumer at a dependency's committed collection-wrapper index so it defers to the dependency's wasm wrappers instead of re-minting them. Repeatable; each value is <dep>=<path/to/collections.rs> (a malformed value, a <dep> that names no extern dependency, or an index file with a line that is not pub use …::<Name>; / blank / //-comment, aborts generation). Validation runs in every mode — including --wasm=false, where the deferral itself is inert (no wasm crate to defer for) but a malformation is still a hard error.
Every wasm-emitting run writes wasm/src/generated/collections.rs — a self-validating index of one pub use crate::…::<Wrapper>; line per collection-wrapper class the crate mints (see Output format). Point this flag at a dependency's copy of that file. For each wrapper the consumer would mint whose element / key / value types are all extern types of <dep>, if its structurally-derived name appears in <dep>'s index the consumer emits a plain use <dep_wasm>::collections::<Name>; (routed through --extern-wasm-crate, or the dep's rust crate name when unmapped) instead of a local class. Two crates each defining the same #[wasm_bindgen] wrapper would otherwise fail to link a single wasm cdylib with a duplicate symbol: __wbg_<wrapper>_free.
- An all-extern wrapper of one dependency whose name is absent from that dep's index is minted locally, with an stderr warning naming it (a dep-side inventory change that shifts ownership back to the consumer is then loud in the regen log).
- A mixed-element wrapper (a consumer type, or types from more than one dependency) is always minted locally — it can only live in the consumer — and so is a wrapper whose emitted class name is not the structural name of its own elements (a nested rule like
arr_idx_foo_list = [* idx_foo_list], whose structural name isIdxFooListListbut whose class isArrIdxFooList). Both are silent while the name is the consumer's alone. If the name the class is actually emitted under also appears in a dependency's index, the mint is announced on stderr instead: the deferral question was answered correctly (this wrapper belongs to the consumer) but the name is then exported as a#[wasm_bindgen]class by both crates, which duplicate-symbol when linked into one cdylib. Remedy: rename the rule, give it a distinct@name, or settle the name on one owner — declare the shape in the dependency's spec, or drop it from the dependency. - A rule of your own whose ident coincides with a name the dependency indexes (
idx_foo_list = [* idx_foo]where the dep already publishesIdxFooList) is unified with the dependency's class rather than minted twice: on the wasm surface your rule name resolves to<dep_wasm>::collections::IdxFooListfrom every reference position — inline members and by-name references alike — while the rust crate keeps thepub type IdxFooListyour rule declares. This keeps the pair linkable, and it is announced on stderr, because a rule that was meant to be a different type sharing the name would silently lose its own wasm class. Remedy in that case: rename the rule, or give it a distinct@name. - A table rule of your own is the one shape this cannot unify: a rule-declared
{* k => v}always keeps the consumer's own class (its wrapper is minted through the rule, not through the deferral seam). If the dependency's index lists that name too, both crates export a#[wasm_bindgen]class of it and the two duplicate-symbol when linked into one cdylib — so this is warned on stderr as well. Remedy: rename the rule, give it a distinct@name, or drop the rule and let the dependency own the type. - A type marked
_CDDL_CODEGEN_EXTERN_TYPE_or_CDDL_CODEGEN_RAW_BYTES_TYPE_in the consumer's own spec (a one-type re-export of a sibling crate's type) has no dependency to key on and no index to consult, so this flag cannot reach its wrappers. That case — including a sibling whose class is hand-written, and therefore in no generated index — is@extern_companions.
The consumer's own collections.rs lists only the wrappers it mints itself; deferred wrappers are brought in by a plain private use (never re-exported), so they never enter the consumer's public API and each crate's index is a precise inventory of what it owns — which is exactly what its consumers point --extern-wrapper-index at.
Regeneration order: the dependency must be (re)generated before the consumer — the index is committed generated output and part of the dependency's cross-crate interface, so it must reflect the dependency's current wrapper inventory when the consumer reads it. This matches the existing multi-crate regeneration pattern (a driver script regenerating crates in dependency order).
Typically paired with --extern-wasm-crate (which supplies the <dep_wasm> crate name the deferred use is qualified through). All flags default off; output with no new flags is byte-identical to before.
cddl-codegen --input=example --output=export --common-import-override=cml_core \
--extern-wasm-crate=cml_core=cml_core_wasm \
--extern-wrapper-index=cml_core=../cml-core/wasm/src/generated/collections.rs
--workspace-depMarks an extern dependency as a co-generated workspace member. For every collection wrapper whose element types are all owned (transitively — nested lists/maps resolve to their named leaves) by that single dependency, the consumer defers unconditionally — it emits use <dep_wasm>::collections::<Name>; at use sites (routed through --extern-wasm-crate) and never mints a local #[wasm_bindgen] class, regardless of any --extern-wrapper-index. This closes the sibling-collision class: two consumers each minting the same FooList would otherwise define one JS class twice and fail to link a single wasm cdylib. Repeatable; each value is a bare <dep> name (the <dep>=<host> host form for unmodifiable external deps is reserved but not yet supported). Each named dependency must be a configured extern dependency and have an --extern-wasm-crate mapping.
The consumer additionally emits wasm/src/generated/borrowed_collections.rs whenever the flag is present (empty-but-present when nothing is borrowed — stable presence, stable diffs): the mirror of collections.rs ("what I provide" ↔ "what I borrow, from whom"). It carries a private #[allow(unused_imports)] mod borrowed of use lines (the compile-checked half — a wrapper a dependency stops providing fails this crate's build, naming the type) and a BORROWED_SHAPES table whose first column is the dependency's rust crate name (see Output format).
The consumer also emits rust/src/generated/borrowed_key_types.rs (same presence semantics), the rust-crate analog for map-key derives. A consumer map keyed on a dependency's type needs that type to derive Eq/Ord/PartialOrd (plus Hash under --preserve-encodings), but the derive lives in the dependency's crate — and when the map mixes a dependency key with a consumer-owned value ({* dep_key => my_local}) the wrapper is not all-one-dep, so it never enters borrowed_collections.rs, yet the dependency must still key-derive dep_key. This file records every such borrowed key type: a _borrowed_key_types_self_check fn (the compiled half — a dependency dropping a derive fails this crate's build, naming the type) and a BORROWED_KEY_TYPES table of (dep rust-crate name, cddl ident) rows the dependency re-reads via --key-requests. When a borrowed key carries a @used_as_key flavor (hash/ord) rather than the bare full bundle, rows gain a third flavor column and the self-check splits into per-flavor bound carriers, so a hash-only borrow is never checked against Ord — see Output format for the format details and the version seam. Because this sidecar is a rust-crate concern, --workspace-dep is honored mode-independently — the flag (its validation and this sidecar) applies under --wasm=false exactly as under --wasm=true; only the wasm-side collection-wrapper deferral and borrowed_collections.rs above are gated on --wasm.
- When the workspace-dep is also the
--common-import-overridevalue, a consumer map keyed onintrecords the built-inIntas a borrowed key row(<override>, "int"[, flavor])— the one reserved CDDL name that reaches this sidecar (see--common-import-overrideabove). - Ownerless wrappers (no named element types, e.g.
{* uint => text}) and mixed-dep wrappers are never workspace-borrowed — they keep the shipped--extern-wrapper-indexdeferral / local-mint behavior. Passing both flags for the same dependency is legal: workspace-borrow the owned shapes, index-defer the ownerless ones. - A consumer's own rule-declared type whose name equals a wrapper it would otherwise borrow is never suppressed; it mints locally and generation warns. The warning states the consequence that always follows — the local class and the dependency's are distinct types across the package boundary despite being structurally identical, so values of that type cannot be passed between the two packages — and the one that follows only sometimes: if the dependency's wasm crate also exports that name (its own spec declares it, or another consumer's request sidecar asked it to mint one) the two duplicate-symbol at link. The generator cannot tell which case you are in: placement is decided from the constituents' owner set alone, and a shadowed wrapper is deliberately never written into
borrowed_collections.rs, so it is never requested from the dependency either. Remedy either way: rename the rule or give it a distinct@name. Only authored rules count as rule-declared here: a named table's auto-synthesizedkeys()-list wrapper ({ * dep_key => v }→DepKeyList) is generator-created, so it borrows like any all-one-dep structural wrapper — silently, with itsuse <dep_wasm>::collections::…import routed into the module holding the table's class.
The sidecar it emits, the dependency-hosted requested_collections.rs, and the reverse-dependency-order regeneration contract (holistic regen = one pass, zero diff when unchanged; a consumer-alone regen that adds a borrow fails loudly until the dependency regens; removals are a benign superset) are documented under Output format § Workspace mode.
Requires --extern-wasm-crate. Defaults off; output with no new flags is byte-identical to before.
cddl-codegen --input=example --output=export --common-import-override=cml_core \
--extern-wasm-crate=cml_core=cml_core_wasm \
--workspace-dep=cml_core
Shapes this flag does not cover (the manual override). --workspace-dep automates only all-one-dep wrappers that generated consumer code borrows. For a mixed-dep wrapper (elements from more than one dependency) or a wrapper needed by hand-written consumer code that no generated code requests, the zero-tool-change interim remains the answer and stays useful permanently: declare the wrapper as a rule in the owning dependency's spec, named to match the structural name (stake_credential_list = [* stake_credential] → StakeCredentialList; for a mixed-dep shape, declare it in the latest involved dependency's spec — the general placement rule applied by hand). A flavored shape must carry its @duplicates directive in the rule's comment, because the structural name encodes the container: pair_map_epoch_to_text = {* epoch => text} ; @duplicates preserve derives PairMapEpochToText over the PairMap twin, while the same rule without the directive derives the loose MapEpochToText — a name no consumer of the preserve table is looking for. Rule-declared wrappers land in that crate's collections.rs index by construction, so every consumer's --extern-wrapper-index deferral picks them up. The not-in-index stderr warning prints the exact paste-able rule line — <snake_name> = <shape> ; requested by <consumer>, or <snake_name> = <shape> ; @duplicates preserve @doc requested by <consumer> for a flavored one (prose after a directive belongs in @doc, so the attribution rides there and lands as a doc comment on the declared type) — telling you precisely which rule to add and where.
Degenerate case: the shared host crate (documented, not yet implemented). For an extern dependency that cannot be modified (external, separately released), the same mechanism degenerates to a designated host — a workspace member with an empty or minimal spec whose sole purpose is to own wrappers over the external dependency's types. Consumers would point the placement at the host, their sidecar entries would name it, and the host's regen would consume --wrapper-requests exactly as any dependency does (regenerated last, since every consumer depends on it). The <dep>=<host> value form for --workspace-dep is reserved for this; the host defaults to the dependency itself. Not yet implemented — until then, the manual override (declare the wrapper as a rule in a commonly-depended workspace crate's spec and index-defer) covers the case.
--wrapper-requestsThe dependency-side companion to --workspace-dep: one <consumer>=<path> per consumer, each pointing at that consumer's committed wasm/src/generated/borrowed_collections.rs. The dependency parses each sidecar strictly (any content outside the frozen format — a compile_error!/unpreserved-comment trap, an unknown line, a mangled row — is a hard error naming the file), takes the entries addressed to itself (dep column == this crate's normalized --lib-name), unions the requested collection-wrapper shapes across consumers, and emits every requested wrapper it does not already produce into wasm/src/generated/requested_collections.rs — indexed in its own collections.rs, each carrying a /// Generated at the request of: <consumers, sorted>. doc. This hosts the wrapper in the dependency so sibling consumers import one definition instead of each minting a colliding #[wasm_bindgen] class.
- The union is keyed by the wrapper's shape, not its name; the derived structural name is cross-checked against the listed name. Hard errors (each with an actionable message): a shape referencing an element the dependency doesn't own; a directly wasm-exposable shape (it lowers to a bare
Vec<…>with no wrapper class — the stub-fidelity diagnosis below); a name↔shape mismatch (the message names each leaf element and how the dependency resolves it); a shape the dependency already produces under a non-structural RULE-DECLARED name (remedy: rename the rule,@nameit, or drop it — expressible precisely because a rule exists; an ANONYMOUS generic-collection instance like an inlineset<elem>field never triggers this: its wrapper lowers to the structural name, so such a request is satisfied own-spec automatically); two requested shapes deriving the same structural name; a nested shape whose inner wrapper is neither requested nor own-produced; a malformed sidecar (including a reserved CDDL/rust identifier as an element, or pathological shape nesting). - A requested shape the dependency already produces under its structural name is satisfied by the existing class — nothing is emitted for it.
- Output is byte-identical regardless of the order of the
--wrapper-requestsflags (everything is sorted before emission). Repeatable;<consumer>is a label used only in attribution and error messages. - A path with no file at all is the cold-workspace case, not an error. It means that consumer borrows nothing — exactly what a consumer that has never been generated records. Tolerating it is what makes an empty workspace bootstrappable: this flag wants the consumer generated first, while
--extern-importwants the dependency generated first, so on a cold tree neither crate could go first and nothing would start. The absence is announced on stderr, because the other way to reach it is a wrong path, which would otherwise silently disable the whole hosting channel. A file that exists but cannot be read, or whose content is outside the frozen format, stays a hard error: that is not "no sidecar", it is one this run cannot honour.
The stub-fidelity contract. Consumer and dependency derive wrapper names independently — the consumer from its _CDDL_CODEGEN_EXTERN_DEPS_DIR_ stub, the dependency from its real spec — so the two agree only when the stub is representation-faithful: declare _CDDL_CODEGEN_EXTERN_TYPE_ only for types the dependency actually exports as wrapper classes, and spell transparent types truthfully — a primitive alias as its definition (coin = uint, transaction_index = uint .size 2), a c-style value enum as its choices (fe = 0 / 1 / 2), and never stub an @no_alias rule as an opaque extern. An unfaithful stub makes the consumer borrow a wrapper the dependency has no class for; the dependency's regen then hard-errors with a stub-fidelity diagnosis naming the element, how the dependency resolves it, and the stub fix (after which the regenerated consumer stops borrowing that shape). One operational red flag: [+ …]/{+ …} NonEmpty shapes always get wrapper classes, so a NonEmpty request hosting cleanly while its loose [* …] twin errors is the signature of a stubbed-opaque transparent element.
With no --wrapper-requests flag the output is byte-identical to before (the file is not emitted).
The requested_collections.rs file format (attribution docs, index re-export, NonEmpty… support wrappers) and the reverse-dependency-order regeneration contract this flag participates in are documented under Output format § Workspace mode.
cddl-codegen --input=cml_core.cddl --output=export --lib-name=cml-core --wasm=true \
--wrapper-requests=cml_chain=../cml_chain/wasm/src/generated/borrowed_collections.rs \
--wrapper-requests=cml_multiera=../cml_multiera/wasm/src/generated/borrowed_collections.rs
--key-requestsThe dependency-side companion to a consumer's rust/src/generated/borrowed_key_types.rs (the map-key-derive channel — see --workspace-dep above): one <consumer>=<path> per consumer. The dependency parses each sidecar strictly (any content outside the frozen format — a compile_error!/unpreserved-comment trap, an unknown line, a mangled row — is a hard error naming the file), takes the rows addressed to itself (dep column == this crate's normalized --lib-name), resolves each borrowed CDDL ident to a type in its own spec, and marks it used-as-key before finalize computes the key-derive set. The dependency then derives the requested traits on that type and — since finalize expands the seed transitively through the type's private fields — on everything it recursively contains. A two-column (bare) row requests the full bundle: Eq/Ord/PartialOrd (plus Hash under --preserve-encodings); a row carrying a @used_as_key flavor column (hash/ord) requests exactly that family, so e.g. a hash-only key never forces Ord through types that cannot supply it.
This covers the case the all-one-dep sidecar structurally can't: a consumer map mixing this dependency's key with a consumer-owned value ({* dep_key => my_local}) never enters borrowed_collections.rs, so only --key-requests conveys that dep_key must be key-capable. (An all-one-dep struct-keyed map {* dep_key => uint} does record its wrapper in borrowed_collections.rs, and the dependency also seeds that key from the --wrapper-requests shape — so passing both flags is the norm.)
- A row naming a type the dependency no longer defines is a hard error naming the consumer and file (a consumer keying on a deleted type must be loud, mirroring the sidecar's compiled self-check). Rows addressed to other dependencies are ignored.
intis the one reserved CDDL ident accepted as a row: it names the built-inInt(which a--common-import-overrideconsumer re-exports from this crate), so anintrow makes this crate emit key-flavoredInteven when its own spec never referencesint. Every other reserved ident stays a hard error.- Output is byte-identical regardless of the order of the
--key-requestsflags (seeds are collected into a sorted set). Repeatable;<consumer>is a label used only in error messages. With no--key-requestsflag the output is byte-identical to before. - A path with no file at all is the cold-workspace case — a stderr warning, not an error; same contract and rationale as
--wrapper-requestsabove.
cddl-codegen --input=cml_core.cddl --output=export --lib-name=cml-core --wasm=true --preserve-encodings=true \
--key-requests=cml_chain=../cml_chain/rust/src/generated/borrowed_key_types.rs \
--key-requests=cml_multiera=../cml_multiera/rust/src/generated/borrowed_key_types.rs
--extern-importConsume a dependency's committed extern-interface export (extern-interface/<dep>/**, emitted by that dependency's own regeneration — see Output format). This is how a dependency that has an export is declared; a physical stub tree under _CDDL_CODEGEN_EXTERN_DEPS_DIR_/<dep>/ is how one without an export is. Repeatable; each value is <dep>=<path/to/extern-interface/<dep>> (a value with no = aborts generation, like the other cross-crate flag parsers). Each mapped path is read and the rules your spec needs are concatenated with _CDDL_CODEGEN_EXTERN_DEPS_DIR_ scope markers, so they land in the same non-exported <dep> scope a physical stub tree would — after which the whole extern-deps pathway (scope filter, extern resolution, wasm crate mapping, request sidecars) is unchanged. Because the export carries the dependency's final Rust names as @rust_name pins, the consumer reads names instead of re-deriving them, eliminating the cross-version naming-skew class.
The needed set is computed, never declared. A dependency's export is complete — it states that dependency's whole surface once, independent of how many consumers read it — while a consumer needs a slice of it, and the slice is derivable from two things the run already holds: the names your spec references and does not define itself, plus whatever those transitively reference through the export's own rule bodies. Only transparent export rows (aliases, named collections, value enums, plain-group bodies) carry those transitive edges; an opaque _CDDL_CODEGEN_EXTERN_TYPE_ / _CDDL_CODEGEN_RAW_BYTES_TYPE_ row references nothing, so the closure is shallow in practice.
Two consequences worth stating outright:
- An export rule nothing reaches is inert. It is never imported and never enters your spec's flat CDDL namespace, so a dependency whose spec grows new rules cannot break an existing consumer — and your spec may define a rule name the dependency also defines, as long as you do not need the dependency's one. That is how a consumer hand-owns a dependency type it re-exports itself: declare your own rule as
_CDDL_CODEGEN_EXTERN_TYPE_andpub usethe dependency's Rust type from your crate root. - What remains is genuinely ambiguous, and is refused by name. A rule you do need whose export body pulls in a name you also define cannot be resolved either way silently; neither can one name needed from two dependencies' exports at once. Both are hard errors naming the chain and the remedies, in place of the flat namespace's bare
rule "X" is already defined.
Flag-fed files are strictly parsed — in full, whether or not any of their rules are needed, because the seam's guarantees are per file: each must begin with the versioned seam header (; _CDDL_CODEGEN_EXTERN_INTERFACE_ v1), must parse on its own, and must carry only recognized @-annotations. The hard errors (each naming the flag value or the offending file):
- a malformed value (no
=separating<dep>from the path); - a missing version header, or an unknown version (a header naming a version this cddl-codegen does not understand — the seam is versioned, so bump the tool);
- an unknown
@-annotation token (a typo or a newer dialect — the strict seam refuses to silently misread it); - an export file that does not parse standalone (export files are self-contained by construction, so a failure here is a defect in the dependency's export — regenerate it);
- a needed rule whose export body references a name your own spec also defines, or one name needed from two dependencies' exports (the two cases above);
- declaring
<dep>here and as a physical_CDDL_CODEGEN_EXTERN_DEPS_DIR_/<dep>/input directory (ambiguous double declaration, never a merge — a dependency is declared exactly once). A--configrun refuses the same shape during expansion, before any crate generates, naming the config key that declared the edge instead of a flag value it derived; - a path that does not exist or contains no
.cddlfiles.
A declared dependency your spec reaches nothing in is not an error — that is an ordinary state of a workspace being wired up — but it is announced on stderr, since the other way to reach it is a wrong <dep> key.
A physical hand-stub under the extern-deps directory remains supported (same dialect, parsed leniently) as the declaration for a dependency that has no export: a hand-written crate, one you cannot regenerate, or one generated by a deliberately separate pass. It is a whole-dependency declaration and an alternative to this flag, not a per-rule supplement to it — the exactly-once rule above is what makes that so.
Determinism. Same input category and wording as --extern-wrapper-index / --wrapper-requests: the export is an explicit cross-crate input, so same inputs → same bytes holds.
Interaction with the other extern-dep flags is by design unchanged — the flag only changes where <dep>'s stub text comes from, not how it is keyed. The imported rules land in the same non-exported <dep> scope with the dependency's original CDDL idents, so --extern-wasm-crate (wasm-crate qualification), --extern-wrapper-index / --workspace-dep (collection-wrapper deferral), and the --wrapper-requests/--key-requests sidecar resolution all key on <dep> and those idents exactly as they do against a physical stub. Combine --extern-import with any of them for the same dependency.
Regenerate the dependency before the consumer so its export is fresh — the same discipline --extern-wrapper-index demands. A consumer referencing an identifier absent from the export gets a hard error, augmented (never swallowed) with the declared dependencies, their export paths, and the remedies — all of which are on the dependency's side, because a dependency you import is already declared and cannot be supplemented rule by rule:
- regenerate the dependency, if the export predates its current spec;
- if the ident is recorded as a
; unexported:rule, fix the cause in the dependency's own spec — a type the dependency hand-owns travels once the dependency itself declares it as_CDDL_CODEGEN_EXTERN_TYPE_— or report the projection limitation; - and if the dependency cannot be regenerated at all, switch that dependency to a whole-dep hand-stub, which means dropping its
--extern-importrather than adding a stub beside it.
cddl-codegen --input=cml_chain.cddl --output=export --lib-name=cml-chain --wasm=false \
--extern-import=cml_core=../cml-core/extern-interface/cml_core
--wasm-cbor-json-api-macroIf it is passed in, it will call the supplied externally defined macro on each exported type, instead of manually exporting the functions for to/from CBOR bytes + to/from JSON API.
The external macro is assumed to exist at the specified path and will be imported if there are module prefixes.
The macro must take the wasm wrapper type as the only parameter.
This macro will be called regardless of the values of to-from-bytes-methods / json-serde-derives / etc, so it is assumed that whatever logic your macros have is consistent with the other CLI flag values.
If the macro's expansion names a collection type (BTreeMap, OrderedHashMap, NonEmptyVec, NonEmptyMap), it must fully-qualify it ($crate::... / std::collections::BTreeMap) rather than rely on the call-site file's ambient imports — imports are emitted only for names the generated code itself references, and ident-scanning cannot see through macro expansion (the in-repo stub macro crate already complies and is compile-gated).
The same rule applies to JsError, JsValue, and wasm_bindgen. The unused-import prune removes those from any generated wasm file that does not itself name them, so a file whose only reason to hold JsError in scope would be this macro's expansion will have that import pruned. All three configured wasm macros — this one, --wasm-conversions-macro, and --wasm-list-macro — must therefore be written with fully-qualified paths (wasm_bindgen::JsError::new(..), #[wasm_bindgen::prelude::wasm_bindgen]) and must not assume JsError / JsValue / wasm_bindgen are in scope at the call site. The in-repo stub macros (tests/wasm-macro-crate) comply (all three fully-qualify), and this is compile-gated.
cddl-codegen --input=example --output=export --wasm-cbor-json-api-macro=cml_core_wasm::impl_wasm_cbor_json_api
--wasm-conversions-macroIf it is passed in, it will call the supplied externally defined macro on each exported type, instead of manually exporting the rust/wasm conversion traits.
The external macro is assumed to exist at the specified path and will be imported if there are module prefixes.
The macro must take the rust type as the first parameter and the wasm wrapper type as the second one.
If the macro's expansion names a collection type (BTreeMap, OrderedHashMap, NonEmptyVec, NonEmptyMap), it must fully-qualify it — never rely on the call-site file's ambient imports (see --wasm-cbor-json-api-macro above; ident-scanning cannot see through macro expansion).
cddl-codegen --input=example --output=export --wasm-conversions-macro=cml_core_wasm::impl_wasm_conversions
--wasm-list-macroIf it is passed in, it will call the supplied externally defined macro on each generated WASM list wrapper (Vec<T>-backed), instead of emitting the inline struct + accessor block (new/len/get/add) + conversion traits. Map wrappers are unaffected.
(The list wrapper types themselves exist for ownership safety at the wasm boundary — see Wasm Differences § Heterogeneous Arrays; this flag only changes how their boilerplate is emitted.)
The external macro is assumed to exist at the specified path and will be imported if there are module prefixes.
The macro must take five parameters:
- the rust element type
- the wasm element type
- the wasm wrapper name
- a
needs_intobool (whether the element crosses the wasm boundary via.into()) - an
is_copybool. e.g.impl_wasm_list!(cml_lib::Foo, Foo, FooList, true, false);
The first two parameters can be generic types (e.g. Vec<u8>, BTreeMap<u64, String>), so match them with :ty fragments (not :path/:ident); the third is an identifier (:ident).
This supersedes --wasm-conversions-macro for list wrappers (the list macro is expected to emit the conversion traits itself); non-list types are unaffected. A list whose element type can't be reduced to those two bools (e.g. an optional element) falls back to the inline form.
The restricted non-empty list wrappers ([+ T] → NonEmptyBarList; see Wasm Differences § Non-empty containers) always emit inline in this phase — only the loose builder wrapper takes the macro path. The restricted wrapper's boundary surface (the failable try_from door, new(first), checked removal) differs from the plain new/len/get/add block the macro emits, so a parallel macro for it is deferred; there is no flag needed to opt into the inline form.
If the macro's expansion names a collection type (BTreeMap, OrderedHashMap, NonEmptyVec, NonEmptyMap), it must fully-qualify it — never rely on the call-site file's ambient imports (see --wasm-cbor-json-api-macro above; ident-scanning cannot see through macro expansion). Types passed as macro arguments are unaffected — call-site tokens are scanned.
(The no-argument form of this flag is reserved for a future built-in default that would also emit the macro definition; for now the macro definition must be supplied by you, e.g. cml_core_wasm::impl_wasm_list.)
cddl-codegen --input=example --output=export --wasm-list-macro=cml_core_wasm::impl_wasm_list
--emit-testsEmits a #[cfg(test)] mod cddl_generated_tests into the generated rust crate, with two kinds of tests derived from each type's structure:
- Round-trip tests (
roundtrip_<type>): for every constructible type, value cases derived from the type itself — a valid baseline, bound boundaries, one case per choice variant, and each optional field additionally present — each asserted byte-identical through the full wire cycle (value → to_cbor_bytes → from_cbor_bytes → to_cbor_bytes). - Reject tests (
reject_<type>): for every type with a bounded (RangeCheck) field, boundary values are accepted and out-of-bounds values rejected — on deserialization for structs (mutate apubfield out of bounds, serialize, confirmfrom_cbor_bytesrejects it), and at the constructor for type/group choices and bounded@newtypewrappers.
Requires --to-from-bytes-methods. Values are minted deterministically from the type structure (no proptest/Arbitrary dependency is added to the generated crate). Shapes that can't be cheaply constructed — bounded nint fields, transparent table/array aliases at top level, types referencing user-supplied code — are skipped with a logged notice, never silently.
A table whose key domain carries a value window ({* int .ne 0 => uint}, {* uint .ge 5 => tstr}) has its synthesized keys minted inside that window rather than counting up from 0, so the emitted round-trip is not defeated by the generated decoder's own — correct — RangeCheck. The window is read through the same classifier that emits that check, and for a nint key it is first translated into the u64 magnitude the key is actually stored as. Two cases are skipped with a logged notice instead of minted: a window that no run of distinct keys can satisfy, and a window that constrains a length rather than a value (a .size-bounded tstr/bytes key), which the synthesized key spellings cannot steer. The enclosing map is then minted empty and its key wire path is left unexercised.
With directory (multi-file) input, the test module still lands at the generated root and covers the types of every module: it glob-imports each generated submodule (use super::<module>::*;), so cross-module values (a type in one module holding a type from another) are constructed and round-tripped like root-scope ones. Two submodules exporting the same type name would make those glob imports ambiguous (rustc E0659) — avoided by not reusing a rule name across modules.
When --preserve-encodings is also set, each round-trip case additionally runs an encoding-fidelity check: a small self-contained CBOR mutator (emitted alongside the tests) derives irregular re-encodings of the minted value's canonical bytes — non-minimal header widths, indefinite array/map framing, chunked strings, reversed map key order, widened float heads, and all of these composed — and asserts every variant decodes and re-encodes byte-identically (the --preserve-encodings contract). The float-widening class applies to every type, whatever CDDL float names it carries: a float name is a set of VALUES, not of encodings (see Floats), so widening a float head produces another encoding of the same value and every name that admitted it still does. With --canonical-form also set, each variant is additionally asserted to canonicalize to the same bytes (encoding-invariance) plus a per-case canonical fixed point. Types whose wire format is partly user-supplied (@custom_serialize/@custom_deserialize, reachable through the type's fields) are excluded from this check, since their accept/re-emit behavior is not the generated serializer's. All variant classes run for every other type, including variable-length arrays/maps of CBOR major-type-7 elements or keys (bool, nil, float, or an optional position): the generated indefinite-length break-check probes for the 0xff break with the non-consuming special_break(), so such an element/key is read normally rather than mistaken for the break.
When --wasm=true is also set, a matching #[cfg(test)] mod cddl_generated_wasm_tests is emitted into the generated wasm crate. It renders each minted value twice — through the wasm wrapper API and through the cddl_lib:: rust API the wasm crate path-depends on — and asserts a cross-crate byte differential (both to_cbor_bytes() byte-equal, catching a wrong wasm-boundary conversion the rust half can't see), a wire round-trip, accessor read-back against the minted literals, and boundary acceptance. It shares the same --to-from-bytes-methods requirement (the wasm CBOR methods it drives), and is additionally skipped when a --wasm-*-macro flag is set (those macros replace the per-type wrapper method surface it targets). Shapes with no faithful wasm build — wrapper/collection ctor args, @newtype/tag wrappers, flatten points — are loud-skipped the same way. cargo check never compiles #[cfg(test)] code, so the emitted module only runs under a cargo test of the wasm crate.
The emitted tests work in a no_std build of the crate, i.e. cargo test --no-default-features --lib on a host. The test module restores std for itself (extern crate std; plus the std prelude, emitted at the top of mod cddl_generated_tests and again in the nested encoding-fidelity module), which it must do locally: the crate root carries #![cfg_attr(not(feature = "std"), no_std)] and is seeded once and then yours, so the tool cannot deliver that line from there. Tests always run on a host, where std exists to be linked; the restore is emitted unconditionally and is a no-op under default features. This combination is covered by a cell of the repo's own no_std_check gate (emit_tests.host_test_nostd), which generates a crate fresh and runs exactly that command.
Two details of that invocation are load-bearing. --lib: the generated crate declares crate-type = ["cdylib", "rlib"], and a cdylib is linked on a host target — with --no-default-features the crate is #![no_std], so that link asks for a #[global_allocator] and a #[panic_handler] and fails before any test runs. --lib builds only the lib test binary. And the tests are not what the no-std-check shim proves: #[cfg(test)] code is never compiled when a crate is built as a dependency, which is the direction that shim checks.
Possible values: true, false
Default: false
cddl-codegen --input=example --output=export --emit-tests true
--emit-tests-conformanceAdds an independent conformance oracle to every --emit-tests round-trip case. Right after a case computes its CBOR bytes, a cddl_conformance::validate(&bytes, "<rule>") call validates them against the source .cddl rule using the cddl crate's validator — a decode + constraint-evaluation path independent of the generated encoder/decoder.
Because the round-trip harness mints its values from the same IR as the code under test, an IR-level miscompile (a bound or member computed wrong at parse time) mints a spec-violating value and then asserts it round-trips green. This oracle closes that gap: the validator rejects the out-of-spec bytes even though the round-trip agrees with itself.
Requires --emit-tests. The generated test crate then needs the cddl dependency and the source spec on disk next to its Cargo.toml as cddl_conformance_source.cddl (the shared oracle helpers from tests/deser_test_conformance.rs — cddl_oracle_load_spec / assert_cddl_conforms — must also be in scope). It shares the fork's parser with the generator, so it catches wrong values, not fork-level misparses, and the validator has known gaps (e.g. it hard-errors on .size over a signed int instead of applying the per-value semantics the RFC author clarified in cbor-wg/cddl#32 — match non-negative values in the uint .size window, non-match negatives; the fork's pinned rev enforces uint control ops and non-uint-endpoint ranges, which released cddl 0.10.x does not). Intended for the maintainer's manual/local IR-conformance sweep, not everyday generation.
Possible values: true, false
Default: false
cddl-codegen --input=example --output=export --emit-tests true --emit-tests-conformance true
--deserialize-depth-limitOpt-in recursion depth guard for the generated deserializers. Generated composite deserializers are recursive-descent, so a recursive type (e.g. tree = [value: uint, children: [* tree]], Plutus-style data) has no intrinsic depth bound: maliciously deep CBOR recurses until the thread's stack overflows and the process aborts (SIGABRT — uncatchable, not a returnable error).
When set to N, every generated composite deserialize acquires an RAII depth guard at entry (a thread-local counter, decremented on every return path including ?-propagation); once nesting exceeds N, deserialization returns a graceful DeserializeError (DeserializeFailure::DepthLimitExceeded) instead of recursing further. The limit is baked into the generated crate at generation time. The guard also covers the AnyCbor runtime type's recursive decoder (the lowering of CDDL any — the position most likely to receive hostile nesting, since it accepts arbitrary CBOR): its recursion seam acquires the same thread-local guard with the same baked limit, so declared and undeclared nesting are bounded uniformly.
This rejects any document nested deeper than N, including spec-valid ones — that is the point of it being opt-in. cddl-codegen must not invent a data limit the spec doesn't have, so there is no default guard: leaving the flag off produces output byte-identical to a build without it. Enable it when your consumer deserializes untrusted input (e.g. on-chain data), where the unbounded default's stack-overflow abort is a DoS. Choose N above the deepest nesting any legitimate document reaches.
This flag makes the generated crate std-only. The guard's counter is a thread_local!, which has no core/alloc equivalent, so a crate generated with it cannot be built with default-features = false. Rather than leaking std silently, the generated serialization.rs carries a compile_error! for that build:
--deserialize-depth-limit output requires the `std` feature (the depth guard is thread_local-based): build with default features (std is default-on), or regenerate without the flag
The std feature is default-on, so this costs an ordinary consumer nothing — the message only appears in a build that explicitly asked for no std. The no-std-check shim such a crate emits is therefore red by design, since that check is a default-features = false build; its own header paragraph says so and quotes the same message, and the attribution guarantee carves this case out by name. Drop the flag if you need a no_std build.
Possible values: any unsigned integer (unset = off)
Default: off (no guard)
cddl-codegen --input=example --output=export --deserialize-depth-limit 128
--no-preserve-commentsDisables the edit-preservation overlay — user comments AND tagged code blocks alike.
By default, when a generated src/generated/** .rs file already exists on disk, the hand edits it carries are re-applied onto the freshly generated content: // cddl-codegen:keep-marked own-line comments are re-anchored by symbol identity and token equality (never a textual diff), // cddl-codegen:insert-start/insert-end blocks re-insert your added code, and // cddl-codegen:replace-start/replaces/replace-end blocks re-apply your code swaps by matching their recorded original in the fresh output. Anything that cannot be safely re-placed is not dropped silently — it is trapped in a tagged compile_error! block that fails the generated crate's build with your original text in the message, and carries forward until you delete it. An own-line comment that is neither this run's output nor keep-marked is trapped the same way: outside a cddl-codegen: block every comment in a generated file is tool-owned, and re-anchoring an unmarked one on a guess corrupts prose. Outside your tagged regions the overlay never touches a generated code token, and running the tool twice equals running it once.
See preserving edits for the block syntax, the anchoring rules, every failure message with its resolution, and the residual limits (trailing comments, tool-owned doc text, unlexable files, orphaned files).
Pass --no-preserve-comments to skip the overlay entirely and clobber each generated file with pristine output. With the overlay off, tagged blocks in the prior output are clobbered like any other edit.
In a config file this is preserve-comments = false. The key is spelled positively because TOML has booleans and a config should not make you write a double negative; omitting it, or setting it to true, leaves the overlay on. See Config file § Booleans.
Possible values: the flag takes no value; its presence disables preservation
Default: preservation on
cddl-codegen --input=example --output=export --no-preserve-comments
--verbosityHow much the run prints. Five ordered levels, each adding to the one below it.
| level | adds |
|---|---|
error | nothing beyond fatal errors, which are the exit path rather than logging and are never suppressed |
warn | warnings, behaviour-change notices, and run output (what the run produced, and what you must act on) |
info | per-file / per-scope / per-phase progress |
debug | the per-rule handling banners |
trace | the full IR dump |
--verbosity error is the quiet mode: a run that succeeds prints nothing at all. It is not a way to hide failure — a fatal error is still reported and the exit code is untouched.
The level decides whether a message appears; the STREAM is decided by its kind, independently. Diagnostics — errors, warnings, and notices that a decode behaviour changed — go to stderr. Run output — what the run is doing, what it produced, what you must now act on — goes to stdout. So: pipe stdout for the run's output, watch stderr for problems. The level governs both streams, so --verbosity error drops a warning from stderr as readily as a progress line from stdout; the one message it cannot drop is a fatal error, which is the exit path rather than logging and is printed at every level.
-v is the short spelling, and it takes a value (-v trace) rather than being repeatable.
Before investigating a confusing generated-crate failure, check the level you ran at. Several of the tool's warnings exist precisely to get ahead of a compile error whose message points somewhere unhelpful — a missing pub mod for a new static runtime file, a recursive type that generates but will not cargo check, a stale hand-written crate root missing a re-export. All of them are diagnostics, so all of them are gone at --verbosity error. If a build broke and the run said nothing, re-run at warn before reading the compiler output.
trace is what every run printed before this flag existed, and it is a debug artifact rather than a report: on a 120-rule spec it runs to ~890 lines and 225 KB, and 94 % of that volume is the {:?} dump of the tool's intermediate representation. Ask for debug when the question is which rules were handled, in what order, and for trace only when it is what did this rule become.
Upgrading: the default output changed. A run that used to print its whole IR now prints only what you must act on, so an existing script's log goes quiet on the first run after the upgrade. Nothing else moved: no message was deleted or reworded, and no generated byte depends on the level. --verbosity trace brings the whole message set back — with the handful of them that are diagnostics now on stderr rather than stdout, since the stream is decided by kind.
In a config file this is verbosity = "<level>", settable in [defaults], a [profiles.<name>] table, or one [crates.<name>] table — so a multi-crate run can raise the level for just the crate you are debugging. --verbosity is one of the two generation flags accepted alongside --config (with --static-dir), and it overrides the key for every crate in the run. See Config file § Verbosity is per crate.
Possible values: error, warn, info, debug, trace
Default: warn
cddl-codegen --input=example --output=export --verbosity=info