Skip to content

peat-cli: --set rejects partial proto3 payloads for registered peat-schema types #112

@kitplummer

Description

@kitplummer

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions