Skip to content

feat!: carry Standard Schema issues on InvalidEntity - #5

Merged
btravers merged 2 commits into
mainfrom
feat/invalid-entity-issue-paths
Aug 6, 2026
Merged

feat!: carry Standard Schema issues on InvalidEntity#5
btravers merged 2 commits into
mainfrom
feat/invalid-entity-issue-paths

Conversation

@btravers

@btravers btravers commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reworked per review. The previous approach flattened issues into
"field: message" strings; this keeps them structured.

Why

fromSchema already returns SchemaIssues (readonly StandardSchemaV1.Issue[])
and toInvalidEntity was discarding it. Flattening was lossy — a message can
itself contain ": ", so the path could not be recovered — and it forced a
caller to re-parse a string the library had just serialised from data it held.
@unthrown/standard-schema is already a peer dependency, so adopting its
Issue type costs nothing.

It was also self-inconsistent: instance already propagated structured issues
into zod's channel with real paths.

Changes

  • errors.tsissues: SchemaIssues instead of readonly string[].
  • entity.tstoInvalidEntity passes issues through untouched. construct
    wraps invariant messages as { message } with no path.
  • instance.ts — forwards each issue's own path into ctx.addIssue, so a
    nested field failure now reports ["owner", "slug"] rather than just
    ["owner"]. Two tests pin this.
  • issues.ts (new) — keysOf (Standard Schema permits both bare PropertyKey
    and { key } segments; zod emits the bare form) and renderIssue, used
    only for the add-defect Error message, which has nowhere to put
    structure.

Behaviour

ApiKey.decode({ ...raw, secret: "short" });
// issues: [{ path: ["secret"], message: "Too small: …" }]

Trial.decode(brokenRow);
// issues: [{ message: "trialEndsAt must be after createdAt" }]   ← no path

z.object({ owner: Organization.instance }).safeParse({ owner: { slug: "" } });
// issues: [{ path: ["owner", "slug"], message: "Too small: …" }]

The pathless-invariant case is new information, not just a different encoding:
a whole-entity rule failure is now distinguishable from a field issue that
happened to have an empty path.

Gate

format --check, lint, typecheck (both passes), test (86, 9 files),
knip, build — all green. Rebased onto current main.

Copilot AI lite review requested due to automatic review settings August 6, 2026 17:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Schema issues now render as "<path>: <message>" — dotted for nesting,
array indices as ordinary segments ("secret: Too small: …", "tags.0: …",
"address.city: …") — so a caller can tell which field failed and key a
field-level error response by splitting on the first ": ".

`issues` stays a readonly string[]: the path goes into the string rather
than alongside it, keeping one representation of an issue instead of two
parallel ones, and keeping the change non-breaking.

Whole-object issues (empty path) and `invariants` messages — domain
sentences about the entity, not field complaints — stay unprefixed.
@btravers
btravers force-pushed the feat/invalid-entity-issue-paths branch from 365a889 to 73c24e4 Compare August 6, 2026 20:27
@btravers btravers changed the title feat: prefix InvalidEntity issue strings with the failing field path feat!: carry Standard Schema issues on InvalidEntity Aug 6, 2026
@btravers
btravers merged commit ecc94c5 into main Aug 6, 2026
@btravers
btravers deleted the feat/invalid-entity-issue-paths branch August 6, 2026 20:31
@btravers
btravers removed the request for review from Copilot August 6, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants