Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/orpc-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"temporal-zod": minor
---

Add oRPC support: `temporal-zod` now exports `temporalJsonSchemaInterceptor`,
which you pass to oRPC's `ZodToJsonSchemaConverter` so Temporal validators render
as correct string JSON Schemas (with `format`/`pattern`) instead of the `anyOf`
oRPC would otherwise produce.

`temporal-zod` takes on no dependency on `@orpc/*` — not even a type-only one.
The interceptor is typed structurally, so consumers who don't use oRPC pay
nothing and nothing needs to resolve at typecheck time.

Widen the exported `*_PATTERN` regexes to accept everything `toJSON()` can emit,
which matters now that they are published as an OpenAPI contract. They previously
rejected strings the validators themselves parse:

- The `[u-ca=…]` annotation appended under a non-ISO calendar, for `PlainDate`,
`PlainDateTime`, `PlainYearMonth`, `PlainMonthDay`, and `ZonedDateTime`.
- The full reference-date form `PlainYearMonth` and `PlainMonthDay` serialize to
under a non-ISO calendar (e.g. `2022-12-25[u-ca=hebrew]`). A bare calendar date
is still rejected for both, since the annotation is required in that form.
- Signed six-digit years for years outside 0000–9999 (e.g. `-000753-04-21`).

`ZONED_DATE_TIME_PATTERN` also no longer ends in `\[.+\]`, whose greedy `.+`
spanned both bracket groups and accepted malformed annotations such as
`[not a time zone!][]`.

Note that `PlainDate` still advertises `format: "date"`, which is RFC 3339
full-date and cannot carry an annotation, so a validator that asserts `format`
will reject a non-ISO `PlainDate` even though the `pattern` accepts it.
53 changes: 53 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
preload = ["./test-setup.ts"]
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"oxfmt": "0.55.0",
"oxlint": "1.69.0",
"oxlint-tsgolint": "0.23.0",
"temporal-polyfill": "^1.0.1",
"turbo": "^2.10.7",
"typedoc": "^0.28.20",
"typescript": "^5.9.3"
Expand Down
46 changes: 46 additions & 0 deletions packages/temporal-zod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,52 @@ import { zPlainDate, zInstant } from "temporal-zod/base";

This is backwards-compatible with the pre-JSON Schema versions of `temporal-zod`.

### With oRPC

[oRPC](https://orpc.unnoq.com) generates its OpenAPI documents with its own
`ZodToJsonSchemaConverter` (from `@orpc/zod/zod4`), which re-implements the
Zod → JSON Schema conversion instead of calling `z.toJSONSchema()`. Because a
Temporal validator is a `z.union([...])` under the hood, the converter would
otherwise emit a messy `anyOf` and drop the `format`/`pattern` metadata.

`temporal-zod` exports `temporalJsonSchemaInterceptor` to fix this. Pass it to the
converter and every Temporal validator renders as the correct string schema:

```typescript
import { OpenAPIGenerator } from "@orpc/openapi";
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { temporalJsonSchemaInterceptor } from "temporal-zod";

const generator = new OpenAPIGenerator({
schemaConverters: [
new ZodToJsonSchemaConverter({
interceptors: [temporalJsonSchemaInterceptor],
}),
],
});
```

`temporal-zod` does not depend on `@orpc/*` at all — not even for types. The
interceptor is typed structurally, so if you don't use oRPC you pay nothing and
nothing needs to resolve.

The interceptor is driven by each validator's metadata rather than by a
per-type list, so all eight Temporal types are covered — `Instant`,
`ZonedDateTime`, `PlainDate`, `PlainTime`, `PlainDateTime`, `PlainYearMonth`,
`PlainMonthDay`, and `Duration` — in both the coercing and `*Instance` variants.

Values travel as the plain ISO strings `toJSON()` produces, and the validator on
the receiving end revives them. The published `pattern` accepts everything
`toJSON()` can emit, including the `[u-ca=…]` annotation added under a non-ISO
calendar, the full reference-date form `PlainYearMonth` and `PlainMonthDay` take
under such a calendar, and signed six-digit years.

One boundary is worth knowing: `PlainDate` also advertises `format: "date"`,
which is RFC 3339 full-date and cannot carry an annotation. A validator that
asserts `format` will therefore reject a non-ISO `PlainDate` even though the
`pattern` accepts it. The `format` is kept because it is correct and useful for
the ISO case, which is the overwhelmingly common one.

### With tRPC

If you are using [tRPC](https://trpc.io/), you likely use Zod to validate your inputs and outputs. However, when using it with [Tanstack Query](https://tanstack.com/query), since the Temporal types get mapped to an object, you should ensure that you are using the instance of the Temporal type rather than the one with type coercion. Otherwise, the query cache will not work as expected.
Expand Down
1 change: 1 addition & 0 deletions packages/temporal-zod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"devDependencies": {
"@macalinao/tsconfig": "^3.2.5",
"@orpc/zod": "^1.14.0",
"@types/bun": "latest",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
Expand Down
Loading
Loading