Comment DSL
We have a comment DSL to help annotate the output code beyond what is possible just with CDDL.
@name
For example in an array-encoded group you can give explicit names just by the keys e.g.:
foo = [
bar: uint,
baz: text
]
but with map-encoded structs the keys are stored and for things like integer keys this isn't very helpful e.g.:
tx = {
? 0: [* input],
? 1: [* outputs],
}
we would end up with two fields: key_0 and key_1. We can instead end up with fields named inputs and outputs by doing:
tx = {
? 0: [* input], ; @name inputs
? 1: [* outputs], ; @name outputs
}
Note: the parsing can be finicky. For struct fields you must put the comment AFTER the comma, and the comma must exist even for the last field in a struct.
It is also possible to use @name with type choices:
foo = 0 ; @name mainnet
/ 1 ; @name testnet
and also for group choices:
script = [
; @name native
tag: 0, script: native_script //
; @name plutus_v1
tag: 1, script: plutus_v1_script //
; @name plutus_v2
tag: 2, script: plutus_v2_script
]
Note: @name does not rename a top-level rule or group — the rule identifier itself is the
emitted type name, so foo = uint ; @name bar is rejected with a clean error. To change the
emitted type name, rename the foo identifier. On a plain group rule the trailing comment
belongs to the last group entry, not the rule: grp = (a: uint) ; @name other renames the
field a to other (exactly like grp = (a: uint ; @name other)) — it neither renames the
group nor errors.
@newtype
With code like foo = uint this creates an alias e.g. pub type Foo = u64; in rust. When we use foo = uint ; @newtype it instead creates a pub struct Foo(u64);.
The wrapper always emits an inner-value getter, named get by default:
impl Foo {
pub fn get(&self) -> u64 {
self.0
}
}
@newtype can optionally rename that getter e.g. foo = uint ; @newtype custom_getter emits custom_getter instead of get. Every wrapper struct (bare tag, @newtype, and bounded/range wrappers) exposes this getter plus a new(inner) constructor in both the rust and wasm bindings.
@newtype is redundant on a top-level single-type tag rule (foo = #6.42(text)): those already auto-wrap into the tag-writing wrapper so their standalone to/from_cbor_bytes API does not drop the tag (see current capacities). Adding ; @newtype there produces the same wrapper, not a double wrapper — since the wrapper already has a default get, its only remaining use on such a rule is to rename that getter.
@no_alias
foo = uint
bar = [
field: foo
]
This would normally result in:
pub type Foo = u64;
pub struct Bar {
field: Foo,
}
but if we use @no_alias it skips generating an alias and uses it directly e.g.:
foo = uint ; @no_alias
bar = [
field: foo
]
to
pub struct Bar {
field: u64,
}
@used_as_key
foo = [
x: uint,
y: uint,
] ; @used_as_key
cddl-codegen derives the comparison/hash traits a type needs to serve as a map/set key. For any type
used as a CBOR map key in the spec it infers this automatically; @used_as_key forces the same
derives onto a type even when the spec never keys a map on it — useful when your own utility code puts
the type in a map and you want the generated code to already implement the traits (and, because it
lives in the spec, your hand-written mod.rs files stay untouched across regenerations).
Flavors
Bare @used_as_key derives the tool's full internal key bundle: Eq, PartialEq, Ord, PartialOrd —
plus Hash under --preserve-encodings (that mode's maps are hash-ordered). This bundle is
mode-dependent and matches exactly what an auto-detected CBOR map key gets.
When your downstream code only needs one family, name it — the flavor is mode-independent (an
external HashMap/BTreeMap requirement exists regardless of the encoding flags):
transaction_output = alonzo_format_tx_out / conway_format_tx_out ; @used_as_key hash
| Tag | Derives |
|---|---|
@used_as_key (bare) | Eq, PartialEq, Ord, PartialOrd (+ Hash under --preserve-encodings) |
@used_as_key hash | Hash, Eq, PartialEq |
@used_as_key ord | Ord, PartialOrd, Eq, PartialEq |
@used_as_key hash ord | the union of both |
Naming a flavor lets a type that keys a HashMap downstream avoid an Ord it can never supply (e.g.
a hand-written extern with no total order), instead of failing to compile far from the cause.
Demand is a union. Flavors and the internal bundle can only add derives, never remove them:
tagging a type hash cannot strip the Ord it gets from also being an auto-detected CBOR map key.
The flavor propagates transitively to every type the tagged type contains, exactly like the bare tag.
Strict vocabulary. Only hash and ord may follow @used_as_key. Any other word — a typo, or
trailing prose like @used_as_key marks the output — is a hard error; put prose in @doc.
Compile-time assertions. Every @used_as_key-tagged type — flavored or bare — gets a named
_demand_<rule> function in the generated crate (generated/key_demand_assertions.rs) that asks
the compiler to prove the type supplies the traits its tag demands (for bare, the mode-dependent
full bundle). Beyond turning a distant trait error into a near, named one, the file is the
in-crate breadcrumb from a failing derive back to its cause: demand propagates transitively, so
"why does this struct need Ord?" is answered by the demand roots this file enumerates, each
citing its tag. (Auto-detected CBOR map keys emit no assertion — the generated containers' own
bounds already enforce them.)
Version-skew hazard. A cddl-codegen older than this feature silently ignores the flavor word and
falls back to the full bundle, so a spec relying on a narrow flavor to avoid Ord (etc.) regenerates
the original failure under an old tool. Pin your tool version in-repo. (Requires cddl-codegen with
@used_as_key flavor support.)
@used_as_elem
bootstrap_witness = [
vkey: bytes,
signature: bytes,
] ; @used_as_elem
When you generate WASM bindings, a list used inline in the spec — [* bootstrap_witness] — mints a
loose-list wrapper class (BootstrapWitnessList) so the elements can cross the WASM boundary safely.
But if no rule in your spec actually contains that list, the wrapper is never generated, even though
hand-written downstream code (or another crate) may still want to construct one.
@used_as_elem forces the generator to mint that loose-list wrapper for the tagged type exactly as
if the spec contained an inline [* bootstrap_witness] usage: the structural class
BootstrapWitnessList, its entry in wasm/src/generated/collections.rs, and its registration as an
own-spec-produced shape (so a downstream --wrapper-requests consumer asking for [* bootstrap_witness]
is satisfied by your crate's class instead of minting its own).
The idiomatic use case is being the canonical host of a wrapper class for a type your crate owns: a
downstream crate points --extern-wrapper-index/--workspace-dep at you and imports BootstrapWitnessList
from your collections module rather than re-minting a colliding #[wasm_bindgen] class. Before this
tag you had to add a throwaway "fake" rule (bootstrap_witness_list = [* bootstrap_witness]) purely to
force the class into existence; the tag expresses the intent directly on the element type and avoids
the extra rust-side pub type alias the fake rule would emit.
Notes:
- It is a no-op without
--wasm(the wrapper is a WASM-boundary concern only). - It is rejected if the element is directly WASM-exposable (e.g. a transparent
uint/textalias): such a list lowers to a bareVec<..>at the boundary with no wrapper class, so there is nothing to mint. - It only mints the loose
[* x]list wrapper.[+ x](NonEmpty) and map wrappers are out of scope (a map wrapper cannot be named by a tag on a single element rule).
@custom_json
foo = uint ; @newtype @custom_json
Avoids generating and/or deriving json-related traits under the assumption that the user will supply their own implementation to be used in the generated library.
It works on any rule shape — newtypes, record structs (map/array group rules), and sum types. On a type-choice rule, rule-level directives attach via the trailing comment of the LAST variant:
my_sum =
uint ; @name integer
/ bytes ; @name raw @custom_json
With --preserve-encodings, the type's serde/schemars derives and the #[serde(skip)]
attributes on its encoding fields are omitted together (either alone would not compile).
@custom_serialize / @custom_deserialize
custom_bytes = bytes ; @custom_serialize custom_serialize_bytes @custom_deserialize custom_deserialize_bytes
struct_with_custom_serialization = [
custom_bytes,
field: bytes, ; @custom_serialize custom_serialize_bytes @custom_deserialize custom_deserialize_bytes
overridden: custom_bytes, ; @custom_serialize write_hex_string @custom_deserialize read_hex_string
tagged1: #6.9(custom_bytes),
tagged2: #6.9(uint), ; @custom_serialize write_tagged_uint_str @custom_deserialize read_tagged_uint_str
]
This allows the overriding of serialization and/or deserialization for when a specific format must be maintained. This works even with primitives where CDDL_CODEGEN_EXTERN_TYPE would require making a wrapper type to use.
The string after @custom_serialize/@custom_deserialize will be directly called as a function in place of regular serialization/deserialization code. As such it must either be specified using fully qualified paths e.g. @custom_serialize crate::utils::custom_serialize_function, or post-generation it will need to be imported into the serialization code by hand e.g. adding import crate::utils::custom_serialize_function;.
With --preserve-encodings=true the encoding variables must be passed in in the order they are used in cddl-codegen with regular serialization. They are passed in as Option<cbor_event::Sz> for integers/tags, LenEncoding for lengths and StringEncoding for text/bytes. These are the same types as are stored in the *Encoding structs generated. The same must be returned for deserialization. When there are no encoding variables the deserialized value should be directly returned, and if not a tuple with the value and its encoding variables should be returned.
There are two ways to use this comment DSL:
- Type level: e.g.
custom_bytes. This will replace the (de)serialization everywhere you use this type. - Field level: e.g.
struct_with_custom_serialization.field. This will entirely replace the (de)serialization logic for the entire field, including other encoding operations like tags,.cbor, etc.
Example function signatures for --preserve-encodings=false for custom_serialize_bytes / custom_deserialize_bytes above:
pub fn custom_serialize_bytes<'se, W: std::io::Write>(
serializer: &'se mut cbor_event::se::Serializer<W>,
bytes: &[u8],
) -> cbor_event::Result<&'se mut cbor_event::se::Serializer<W>>
pub fn custom_deserialize_bytes<R: std::io::BufRead + std::io::Seek>(
raw: &mut cbor_event::de::Deserializer<R>,
) -> Result<Vec<u8>, DeserializeError>
Example function signatures for --preserve-encodings=true for write_tagged_uint_str / read_tagged_uint_str above:
pub fn write_tagged_uint_str<'se, W: std::io::Write>(
serializer: &'se mut cbor_event::se::Serializer<W>,
uint: &u64,
tag_encoding: Option<cbor_event::Sz>,
text_encoding: Option<cbor_event::Sz>,
) -> cbor_event::Result<&'se mut cbor_event::se::Serializer<W>>
pub fn read_tagged_uint_str<R: std::io::BufRead + std::io::Seek>(
raw: &mut cbor_event::de::Deserializer<R>,
) -> Result<(u64, Option<cbor_event::Sz>, Option<cbor_event::Sz>), DeserializeError>
Note that as this is at the field-level it must handle the tag as well as the uint.
For more examples see tests/custom_serialization (used in the core and core_no_wasm tests) and tests/custom_serialization_preserve (used in the preserve-encodings test).
@doc
This can be placed at field-level, struct-level or variant-level to specify a comment to be placed as a rust doc-comment.
docs = [
foo: text, ; @doc this is a field-level comment
bar: uint, ; @doc bar is a u64
] ; @doc struct documentation here
docs_groupchoice = [
; @name first @doc comment-about-first
0, uint //
; @doc comments about second @name second
text
] ; @doc type-level comment
Will generate:
/// struct documentation here
#[derive(Clone, Debug)]
pub struct Docs {
/// this is a field-level comment
pub foo: String,
/// bar is a u64
pub bar: u64,
}
impl Docs {
/// * `foo` - this is a field-level comment
/// * `bar` - bar is a u64
pub fn new(foo: String, bar: u64) -> Self {
Self { foo, bar }
}
}
/// type-level comment
#[derive(Clone, Debug)]
pub enum DocsGroupchoice {
/// comment-about-first
First(u64),
/// comments about second
Second(String),
}
Due to the comment dsl parsing this doc comment cannot contain the character @.
Known limitation: on a type choice of fixed values only (a dataless enum, e.g. foo = 0 / 1),
per-variant @doc is currently not emitted; it works on data-carrying variants (e.g. uint / tstr).
CDDL_CODEGEN_EXTERN_TYPE
While not as a comment, this allows you to compose in hand-written structs into a cddl spec.
foo = _CDDL_CODEGEN_EXTERN_TYPE_
bar = [
x: uint,
y: foo,
]
This will treat Foo as a type that will exist and that has implemented the Serialize and Deserialize traits, so the (de)serialization logic in Bar here will call Foo::serialize() and Foo::deserialize().
This can also be useful when you have a spec that is either very awkward to use (so you hand-write or hand-modify after generation) in some type so you don't generate those types and instead manually merge those hand-written/hand-modified structs back in to the code afterwards. This saves you from having to manually remove all code that is generated regarding Foo first before merging in your own.
This can also be useful when you have a spec that is either very awkward to use (so you hand-write or hand-modify after generation) in some type so you don't generate those types and instead manually merge those hand-written/hand-modified structs back in to the code afterwards. This saves you from having to manually remove all code that is generated regarding Foo first before merging in your own.
This also works with generics e.g. you can refer to foo<T>. As with other generics this will create a pub type FooT = Foo<T>; definition in rust to work with wasm-bindgen's restrictions (no generics) as on the wasm side there will be references to a FooT in wasm. The wasm type definition is not emitted as that will be implementation-dependent. For an example see extern_generic in the core unit test.
CDDL_CODEGEN_RAW_BYTES_TYPE
Allows encoding as bytes but imposing hand-written constraints defined elsewhere.
foo = _CDDL_CODEGEN_RAW_BYTES_TYPE_
bar = [
foo,
]
This will treat foo as some external type called Foo. This type must implement the exported (in serialization.rs) trait RawBytesEncoding.
Under --json-serde-derives the type must additionally implement serde::Serialize/serde::Deserialize, and under --json-schema-export also schemars::JsonSchema, since the generated code delegates its JSON representation to the user type.
This can be useful for example when working with cryptographic primitives e.g. a hash or pubkey, as it allows users to have those crypto structs be from a crypto library then they only need to implement the trait for them and they will be able to be directly used without needing any useless generated wrapper struct for the in between.