typespec-http-zod 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/api.js CHANGED
@@ -1360,7 +1360,28 @@ function responseArmsOf(route, response, alternate) {
1360
1360
  * declares. Each arm resolves its own body now, so an arm with no schema is a response the document
1361
1361
  * says carries no body, and saying `undefined` is the truthful answer rather than a guess.
1362
1362
  */
1363
+ /**
1364
+ * **An error arm whose status the primary arm already answers is dropped, primary winning.**
1365
+ *
1366
+ * `errorArmsOf` excludes 2xx from the failure arms, so widening the primary to include 3xx made a
1367
+ * redirect BOTH: `[{ status: 302, schema: undefined, headers: [...] }, { status: 302, schema:
1368
+ * undefined }]`, two arms for one status where the second shadows the first's headers for anything
1369
+ * scanning past the first match.
1370
+ *
1371
+ * Widening `isSuccessKey` to match would be wrong in the other direction: an operation declaring a
1372
+ * 200 AND a 302 has 200 as its primary, so the 302 must stay a failure arm to be emitted at all.
1373
+ * Deduplicating here keeps both cases, because it asks what was actually emitted rather than what
1374
+ * might have been.
1375
+ */
1376
+ const emitted = new Set();
1377
+ for (const arm of arms) {
1378
+ const status = /status: ([^,}]+)/.exec(arm)?.[1]?.trim();
1379
+ if (status !== undefined)
1380
+ emitted.add(status);
1381
+ }
1363
1382
  for (const arm of route.errorArms) {
1383
+ if (emitted.has(String(arm.status)) || emitted.has(JSON.stringify(arm.status)))
1384
+ continue;
1364
1385
  arms.push(`{ status: ${JSON.stringify(arm.status)}, schema: ${arm.schema ?? "undefined"} }`);
1365
1386
  }
1366
1387
  return `[${arms.join(", ")}]`;
package/dist/src/zod.js CHANGED
@@ -337,18 +337,34 @@ const SCALARS = {
337
337
  decimal128: "z.number()",
338
338
  numeric: "z.number()",
339
339
  /**
340
- * Dates cross the wire as strings and are validated as strings.
340
+ * **A declared TYPE is a claim about the value, and these check it.**
341
341
  *
342
- * Not `z.iso.datetime()`: response schemas are permissive on purpose, and a
343
- * stricter format check here would turn a producer emitting a legal-but-unexpected instant into a
344
- * failed response for a caller who could have parsed it perfectly well.
342
+ * These are not strings with a hint attached: they are scalars the document publishes as
343
+ * `format: date-time`, `format: uri` and so on. Emitting `z.string()` for them discarded a
344
+ * declared type, so a service promising a timestamp accepted `banana` - and a consumer who wanted
345
+ * the check had to rewrite the spec as `string` with a `@pattern`, losing `format: date-time` from
346
+ * the document AND getting a weaker check, since a hand-written pattern accepts `2026-02-31`. A
347
+ * rule that pushes consumers into writing worse specs is the wrong rule.
348
+ *
349
+ * **`@format("...")` on a plain string is a different case and stays unenforced.** Under JSON
350
+ * Schema 2020-12, which OpenAPI 3.1 uses, `format` is an annotation rather than an assertion, so
351
+ * enforcing an author's hint would add a rule the contract does not state, and
352
+ * `@format("account-number")` proves no general rule exists. A type is a claim; an annotation is a
353
+ * hint.
354
+ *
355
+ * **`{ offset: true }` is measured, not decorative.** The previous comment here warned that a
356
+ * stricter check would fail "a legal-but-unexpected instant", and it was right about the risk: a
357
+ * bare `z.iso.datetime()` REJECTS `2026-08-14T12:00:00+01:00`, which is valid RFC 3339 and valid
358
+ * `format: date-time`. With the offset permitted, every legal instant is accepted and only
359
+ * `2026-02-31` and `banana` are refused. `local: true` would go too far, admitting a value with no
360
+ * timezone at all, which RFC 3339 does not allow.
345
361
  */
346
- utcDateTime: "z.string()",
347
- offsetDateTime: "z.string()",
348
- plainDate: "z.string()",
349
- plainTime: "z.string()",
350
- duration: "z.string()",
351
- url: "z.string()",
362
+ utcDateTime: "z.iso.datetime({ offset: true })",
363
+ offsetDateTime: "z.iso.datetime({ offset: true })",
364
+ plainDate: "z.iso.date()",
365
+ plainTime: "z.iso.time()",
366
+ duration: "z.iso.duration()",
367
+ url: "z.url()",
352
368
  };
353
369
  function scalarToZod(program, scalar) {
354
370
  // `scalar unixTimestamp32 is utcDateTime;` with `@encode(int32)` on the SCALAR, not on each use.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-zod",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "TypeSpec emitter and library: turn an HTTP service definition into Zod validators and framework-free contract types that agree, keyword for keyword, with the OpenAPI document @typespec/openapi3 publishes from the same source.",
5
5
  "keywords": [
6
6
  "codegen",