Summary
peat create <registered-collection> --set <field>=<value> and peat update <target> --set … fail on registered peat-schema types because prost's serde derive (no #[serde(default)]) rejects missing scalar/enum fields. Operators are forced to --from <complete.json> for the 5 builtin types (capabilities, node-configs, node-states, cell-configs, cell-states); the ergonomic --set path works only on arbitrary-JSON collections.
Repro
$ peat create node-states --id ns-1 --set fuel_minutes=45 --dry-run
peat: malformed request: schema validation failed for NodeState document:
Invalid field value: could not deserialise as NodeState: missing field `health`
health is proto3 enum HealthStatus = 3; — would zero-default to Unspecified on the wire, but prost's serde wants it present in JSON.
Why this happens
crates/peat-cli/src/cli/writes.rs::validate_against_schema calls the descriptor's validate_json(value), which deserializes via prost-derived serde::Deserialize. proto3 messages have implicit zero defaults at the wire layer, but the generated Deserialize impl doesn't carry #[serde(default)], so JSON omission is an error rather than a zero.
Same failure on update: apply_sets overlays partial fields onto the existing doc, so the post-merge JSON still lacks the proto3 zero-defaults for fields the existing doc never had.
Fix shape (recommended)
apply_sets (or a sibling helper invoked before validate_against_schema) pre-populates proto3 zero-defaults from the type descriptor when the target collection is known. TypeDescriptor.fields already enumerates the renderable field set + FieldFormat; extend (or sibling-method) the descriptor to also enumerate the proto3 zero values, then merge them under the user's --set overlay before validation.
Sketch:
fn proto3_defaults(desc: &TypeDescriptor) -> serde_json::Map<String, Value> {
desc.fields
.iter()
.map(|f| (f.name.into(), zero_value_for(&f.format)))
.collect()
}
// in create / update before validate_against_schema:
if let Some(desc) = registry.for_collection(collection) {
let mut base = proto3_defaults(desc);
// user --set overlays last so it wins
for (k, v) in user_value.as_object().unwrap_or(&Map::new()) {
base.insert(k.clone(), v.clone());
}
let merged = Value::Object(base);
validate_against_schema(collection, &merged)?;
}
Sibling option: peat-schema's prost build adds #[serde(default)] to all generated fields (peat-schema-side change). Pro: fixes every consumer, not just peat-cli. Con: cross-repo coupling; consumers that do want strict JSON deserialization lose the ability without further plumbing.
Workaround today
Use --from <path-to-complete-json>. See crates/peat-cli/tests/e2e/scenarios.rs::run_typed_lifecycle for valid minimal JSON shapes per type.
Scope
- Affects: all 5 builtin peat-schema registered types.
- Does not affect: arbitrary-JSON collections (unknown to the registry → no validator → no prost deserialization).
- Tracked: peat-node ADR-001 Open Question §8.
- PR peat-node#107 holds on this + peat-mesh#202 (the sibling CDC contract gap).
Summary
peat create <registered-collection> --set <field>=<value>andpeat update <target> --set …fail on registered peat-schema types because prost's serde derive (no#[serde(default)]) rejects missing scalar/enum fields. Operators are forced to--from <complete.json>for the 5 builtin types (capabilities,node-configs,node-states,cell-configs,cell-states); the ergonomic--setpath works only on arbitrary-JSON collections.Repro
healthisproto3 enum HealthStatus = 3;— would zero-default toUnspecifiedon the wire, but prost's serde wants it present in JSON.Why this happens
crates/peat-cli/src/cli/writes.rs::validate_against_schemacalls the descriptor'svalidate_json(value), which deserializes via prost-derivedserde::Deserialize. proto3 messages have implicit zero defaults at the wire layer, but the generatedDeserializeimpl doesn't carry#[serde(default)], so JSON omission is an error rather than a zero.Same failure on
update:apply_setsoverlays partial fields onto the existing doc, so the post-merge JSON still lacks the proto3 zero-defaults for fields the existing doc never had.Fix shape (recommended)
apply_sets(or a sibling helper invoked beforevalidate_against_schema) pre-populates proto3 zero-defaults from the type descriptor when the target collection is known.TypeDescriptor.fieldsalready enumerates the renderable field set +FieldFormat; extend (or sibling-method) the descriptor to also enumerate the proto3 zero values, then merge them under the user's--setoverlay before validation.Sketch:
Sibling option: peat-schema's prost build adds
#[serde(default)]to all generated fields (peat-schema-side change). Pro: fixes every consumer, not just peat-cli. Con: cross-repo coupling; consumers that do want strict JSON deserialization lose the ability without further plumbing.Workaround today
Use
--from <path-to-complete-json>. Seecrates/peat-cli/tests/e2e/scenarios.rs::run_typed_lifecyclefor valid minimal JSON shapes per type.Scope