ADR metadata/0003: 64-Byte String Chunking Ownership
Decides that generated converter code, not a helper class, owns 64-byte string chunking for Cardano metadata, making the logic explicit and self-contained.
What this file does
Decides that generated converter code, not a helper class, owns 64-byte string chunking for Cardano metadata, making the logic explicit and self-contained.
When to use it
- You generate Java code that writes long strings to Cardano transaction metadata
- You want to avoid hidden dependencies on helper class auto-split behaviour
- You need round-trip compatible metadata reading and writing
- You are designing an annotation processor for Cardano metadata conversion
Assumes this stack
ADR metadata/0003: 64-Byte String Chunking Ownership
- Status: Accepted
- Date: 2026-02-19
- Deciders: Cardano Client Lib maintainers
Context
Cardano transaction metadata (CIP-10) limits each text value to 64 UTF-8 bytes. Strings longer than 64 bytes must be represented as a Cardano list of ≤64-byte chunks, reassembled on read-back.
The project's CBORMetadataMap helper class already contains auto-split logic that silently
chunks strings exceeding 64 bytes before writing to CBOR. This means that if the generated
toMetadataMap simply calls map.put(key, longString), the split happens inside
CBORMetadataMap as a side effect.
The same auto-split has no counterpart in fromMetadataMap: the generated code must
explicitly handle a MetadataList value when reading a key that was stored as chunks.
The problem with relying on CBORMetadataMap's auto-split
-
Invisible behaviour: The generator produces code like
map.put("note", value), but the actual on-chain representation is a list of chunks. Reading that generated code gives no indication that chunking occurs. -
Fragile coupling: The auto-split is an implementation detail of
CBORMetadataMap, not a published contract. It can be changed, removed, or made opt-in without breakingCBORMetadataMap's API. -
Asymmetry: Even with auto-split active,
fromMetadataMapmust still handleMetadataListexplicitly — so half the chunking logic lives in the generator regardless.
Decision
The generated converter owns the 64-byte chunking logic in both directions.
CBORMetadataMap's auto-split is bypassed for generated code by putting the chunked
MetadataList directly, never passing a long string to map.put.
Generated pattern — toMetadataMap
if (order.getNote().getBytes(StandardCharsets.UTF_8).length > 64) {
MetadataList _chunks = MetadataBuilder.createList();
for (String _part : StringUtils.splitStringEveryNCharacters(order.getNote(), 64)) {
_chunks.add(_part);
}
map.put("note", _chunks);
} else {
map.put("note", order.getNote());
}
- Short strings (≤ 64 UTF-8 bytes) are stored directly as Cardano text.
- Long strings are split into ≤64-character chunks using
StringUtils.splitStringEveryNCharactersand stored as aMetadataList. - The byte-length check uses
getBytes(UTF_8)because the limit is bytes, not characters.
Generated pattern — fromMetadataMap
Object v = map.get("note");
if (v instanceof String) {
obj.setNote((String) v);
} else if (v instanceof MetadataList) {
StringBuilder _sb = new StringBuilder();
MetadataList _list = (MetadataList) v;
for (int _i = 0; _i < _list.size(); _i++) {
Object _chunk = _list.getValueAt(_i);
if (_chunk instanceof String) {
_sb.append((String) _chunk);
}
}
obj.setNote(_sb.toString());
}
Both branches are always present, allowing the converter to read data written by:
- The generated converter (which writes chunks only when > 64 bytes).
CBORMetadataMapauto-split (which always chunks; producesMetadataList).- Any other well-formed metadata producer that respects CIP-10.
Alternatives considered
1. Rely on CBORMetadataMap auto-split (rejected)
Generated toMetadataMap calls map.put(key, value) regardless of length; auto-split
handles it silently.
Why rejected:
- The generated code does not reflect the actual on-chain structure; it is misleading.
- Half the logic (
fromMetadataMapMetadataList handling) must live in the generator anyway. - A future change to
CBORMetadataMapbehaviour could silently break round-trips.
2. Always store as MetadataList (rejected)
Every string, regardless of length, is stored as a one-element list.
Why rejected:
- Wastes space for short strings (most strings in practice are short).
- Makes metadata harder to query — tools that read raw metadata expect plain strings for short values.
- Not idiomatic Cardano metadata.
3. Configurable threshold via annotation (rejected)
@MetadataField(chunkSize = 64) lets callers override the chunk size.
Why rejected:
- 64 bytes is a hard Cardano protocol limit, not a stylistic choice.
- Adding configuration for a fixed protocol constant adds complexity without benefit.
Consequences
Positive
- Generated code is self-contained and readable: the chunking logic is explicit in the source.
- No hidden dependency on
CBORMetadataMapbehaviour. fromMetadataMaphandles both plain strings and lists, making it compatible with data written by any CIP-10-conformant producer.
Neutral
- The 64-byte threshold appears in generated code. If the Cardano protocol ever increases this limit, generated code would need to be regenerated (acceptable — protocol limits rarely change).
Negative
- Each String field adds several lines of generated code (the if/else + loop). For classes with many String fields this increases generated source size.
Related
- ADR metadata/0001: Annotation Processor Core Design
- ADR metadata/0002: Java-to-Cardano Metadata Type Mapping
- ADR metadata/0004: @MetadataField(enc=…) Type Override Mechanism
- CIP-10: Transaction Metadata (64-byte text limit)
StringUtils.splitStringEveryNCharacters— utility used in generated code
What's inside
Context, decision, two generated code patterns, three rejected alternatives, consequences, and related ADRs.
Change this for your project
- Replace
order.getNote()with your own model field accessor - Replace
StringUtils.splitStringEveryNCharacterswith your own chunking utility - Replace
CBORMetadataMapwith your project's metadata map class
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Generated code explicitly handles protocol limits rather than relying on hidden helper behaviour
- Both short and long string branches are always present in generated code for forward/backward compatibility
Related Documents
Enso Atlas: Technical Specification
Describes a modular, on-premise pathology AI platform with swappable foundation models, classification heads, and config-driven cancer-type projects.
Annotating documents
Guides human annotators through labeling entity mentions, relations, and coreferences in Watson Knowledge Studio using the ground truth editor.
Marriott Library Metadata Dictionary
Documents 18 metadata fields used by the Marriott Library digital collections, with Dublin Core mappings, Solr field names, and usage guidelines.
redcapAPI (development version)
Changelog for the redcapAPI R package, documenting breaking changes, fixes, new features, and deprecation notices across versions.