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
Related Documents
Dataset Collection Guide for Yellow Boy Detection
This guide covers collecting and labeling images for training a YOLOv8 model to detect "Yellow Boy" (iron hydroxide precipitate) in water bodies.
Annotating documents
lastupdated: "2020-08-04"
redcapAPI (development version)
* `unlockREDCap` upstream dependency on shelter now default to 'SHELTER_' prefix when looking for ENV variable overrides.
Annotating documents
lastupdated: "2020-08-04"