Skip to content

GetFieldLabelsResponse declares rich label entries; both surfaces emit bare strings #3847

Description

@os-zhuang

Found while fixing #3833 (PR #3846). Filing rather than folding in — that one is a correctness fix, this changes the wire shape.

The gap

GetFieldLabelsResponseSchema (packages/spec/src/api/protocol.zod.ts) declares each label as an object:

labels: z.record(z.string(), z.object({
  label: z.string().describe('Translated field label'),
  help: z.string().optional().describe('Translated help text'),
  options: z.record(z.string(), z.string()).optional().describe('Translated option labels'),
})).describe('Field labels keyed by field name'),

Both serving surfaces emit Record<string, string> — a bare label per field:

So a client typed against GetFieldLabelsResponse reads labels[field].label and gets undefined, because the value is the string itself. The declared help and options never appear at all, though the underlying data has them — FieldTranslationSchema carries label, help, placeholder, and options, and bundles populate them. The endpoint drops them on the floor.

Why it survived

The bare-string shape is consistent across both surfaces, so nothing looks inconsistent from the outside — it is only wrong against the schema. And there is currently no consumer to notice: objectui does not call this endpoint, and in-repo callers are limited to the client SDK method plus its wire-dialect tests, which assert URL shape rather than body shape.

Note this is the third declared ≠ enforced gap on this one endpoint family, after #3676 (request filters no server read) and #3833 (a derivation scanning a retired dialect). Whatever check catches this class should probably be pointed at /i18n first.

Options

  1. Emit the declared shape{ label, help?, options? } per field, sourced straight from FieldTranslationSchema, which already has all three. Makes the response match its contract and stops discarding data the bundle carries. Breaking for anyone reading labels[field] as a string; the sweep above suggests that is nobody today, which makes this the cheap moment to do it.
  2. Narrow the schema to Record<string, string> — declare what is served. Smaller, but it locks the endpoint out of ever returning help/options without another breaking change, and those are exactly what a localized form needs.

Option 1 looks right on the same reasoning #3676 used in reverse: there the declared thing could not be delivered and no one wanted it, so it was trimmed; here the declared thing can be delivered, the data is already loaded, and it is the more useful surface. But it is a wire-shape change and should be a deliberate call, not a side effect.

Worth deciding alongside whether placeholder (present on FieldTranslationSchema, absent from the response schema) belongs in the response too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions