CEP 37 specifies metadata.custom_metadata as dict[str, str], free-form key-value string pairs. The current conda-lock implementation enforces this via a StrictModel.
The string constraint is fine for flat annotations (a git SHA, a build ID, a commit URL), but it breaks down as soon as anyone wants to stash a structured payload there.
Concrete example: in conda-lockfiles (conda/conda-lockfiles#132) I'm recording user-requested MatchSpec strings so that exported lockfiles carry user intent, not just the pinned solve. That's a list[str]. The spec forces me to JSON-encode it and embed it as a single string:
custom_metadata:
created_by: conda-lockfiles 0.1.x
requested_specs: '["python_abi", "python >=3.11"]'
versus what the YAML actually wants to be:
custom_metadata:
created_by: conda-lockfiles 0.1.x
requested_specs:
- python_abi
- python >=3.11
The JSON-string form is less diffable, less greppable, and hides structure from anyone casually reading the lockfile. It also means every consumer has to know which keys are JSON-encoded and which aren't.
What I'd like to see
Widen the type to something like dict[str, str | int | bool | list | dict] or dict[str, Any] (effectively "any YAML-scalar-or-container value"). That keeps the intent (free-form user metadata) and removes the string wrapper workaround.
Lockfile writers that don't want to generate structured values can continue to write flat strings and nothing changes for them. Consumers need to handle more value types, but arguably they already need to: conda-lock's LockMeta.__or__ merges custom_metadata dicts today, which would produce surprising results with JSON-string collision anyway.
Why now
CEP 37 is accepted but very recent (March 2026) and conda-lock's implementation is the only one that meaningfully enforces it. Catching this before a second implementation bakes in the same constraint seems worthwhile.
Alternatives considered
- Invent a sibling block. CEP 37 already lists
git_metadata, inputs_metadata, time_metadata as structured cousins of custom_metadata. I could propose a new structured block per use case, but that explodes the spec and misses the point: custom_metadata is the escape hatch, it should actually work as one.
- Keep JSON-strings as the convention. Works, ships today, ugly forever.
Prior art
CEP 37 specifies
metadata.custom_metadataasdict[str, str], free-form key-value string pairs. The current conda-lock implementation enforces this via aStrictModel.The string constraint is fine for flat annotations (a git SHA, a build ID, a commit URL), but it breaks down as soon as anyone wants to stash a structured payload there.
Concrete example: in conda-lockfiles (conda/conda-lockfiles#132) I'm recording user-requested MatchSpec strings so that exported lockfiles carry user intent, not just the pinned solve. That's a
list[str]. The spec forces me to JSON-encode it and embed it as a single string:versus what the YAML actually wants to be:
The JSON-string form is less diffable, less greppable, and hides structure from anyone casually reading the lockfile. It also means every consumer has to know which keys are JSON-encoded and which aren't.
What I'd like to see
Widen the type to something like
dict[str, str | int | bool | list | dict]ordict[str, Any](effectively "any YAML-scalar-or-container value"). That keeps the intent (free-form user metadata) and removes the string wrapper workaround.Lockfile writers that don't want to generate structured values can continue to write flat strings and nothing changes for them. Consumers need to handle more value types, but arguably they already need to: conda-lock's
LockMeta.__or__mergescustom_metadatadicts today, which would produce surprising results with JSON-string collision anyway.Why now
CEP 37 is accepted but very recent (March 2026) and conda-lock's implementation is the only one that meaningfully enforces it. Catching this before a second implementation bakes in the same constraint seems worthwhile.
Alternatives considered
git_metadata,inputs_metadata,time_metadataas structured cousins ofcustom_metadata. I could propose a new structured block per use case, but that explodes the spec and misses the point:custom_metadatais the escape hatch, it should actually work as one.Prior art
LockMeta.custom_metadatatype: https://github.com/conda/conda-lock/blob/main/conda_lock/lockfile/v1/models.py