Skip to content

finding(app-shell): saveObject called without existingFields still PUTs a body ObjectSchema refuses — fields is REQUIRED, and {} is not a safe default #6490

Description

@claude

The severable half of objectui#6240, recorded as its own card because the resolution is a public-signature judgement rather than a shape conversion.

The measurement

ObjectSchema.fields is not merely typed, it is REQUIRED. Measured against the installed @objectstack/spec 17.2.0:

ObjectSchema.safeParse({ name: 'account', label: 'Account' })
=> success = false invalid_type @ fields ("expected record, received undefined")

objectui#6240 fixed the SHAPE: when a caller passes existingFields, toObjectPayload now emits the name-keyed map and the body parses. When a caller does NOT pass it, fields is omitted and the body is still refused — unchanged from before that card, and deliberately so.

Why objectui#6240 did not simply default to {}

Because {} parses green and PUT /api/v1/meta/object/:name is an UPSERT. Emitting {} for a caller that just did not supply the argument would land as "this object has no fields" and delete every field of the object on a save that only meant to rename it — trading a loud, harmless 422 for silent data loss. That is pinned as a positive control (omits fields entirely when the caller supplied none — it does NOT write {}), with an ablation showing that defaulting to {} reds that one case.

The decision this needs

saveObject(obj, existingFields?: FieldMetadataPayload[]) is public API — MetadataService is reachable from app-shell's entry through useMetadataService(): MetadataService | null, so its signature is part of the published surface even though the class is not re-exported by name. Three readings, none of them free:

  1. Make the fields argument required. Honest — the method cannot write a valid document without it — but a breaking signature change for any consumer calling saveObject(obj).
  2. Fetch and merge, the way saveFields already does.saveFields GETs the current document and spreads it, so an object save could preserve the stored fields instead of asking the caller to hand them back. Costs a request; makes the parameter redundant, which then wants retiring on its own terms (ADR-0049 shape).
  3. Leave it and refuse client-side. Throw before the request when no fields are supplied, so the author gets the diagnosis without a round trip. Same outcome as today, better message, still a behaviour change for callers relying on the request being issued.

No in-repo caller passes existingFields at all today (the only call sites are tests), so the population this breaks is entirely external consumers — which is what makes it a judgement rather than a mechanical fix.

Refs: objectui#6240 (the shape half, landed) · objectui#5761 (parity family, coverage note 4)


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions