typespec-http-zod 0.26.0 → 0.27.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/zod.js CHANGED
@@ -11,7 +11,7 @@ import { reportDiagnostic } from "./lib.js";
11
11
  * Zod, rejects everything, and unreachable in practice because the error diagnostic has already
12
12
  * failed the compile that would have written it.
13
13
  */
14
- const UNREPRESENTABLE = "z.never()";
14
+ export const UNREPRESENTABLE = "z.never()";
15
15
  /**
16
16
  * **Installed by the registry, which is the only place that knows what a declared type is CALLED.**
17
17
  *
@@ -339,16 +339,36 @@ const SCALARS = {
339
339
  string: "z.string()",
340
340
  boolean: "z.boolean()",
341
341
  bytes: "z.string()",
342
- int8: "z.number().int()",
343
- int16: "z.number().int()",
344
- int32: "z.number().int()",
345
- int64: "z.number().int()",
346
- safeint: "z.number().int()",
347
- uint8: "z.number().int()",
348
- uint16: "z.number().int()",
349
- uint32: "z.number().int()",
350
- uint64: "z.number().int()",
351
- integer: "z.number().int()",
342
+ /**
343
+ * **A declared WIDTH is a claim about the value too, and ten of them used to share one check.**
344
+ *
345
+ * Every integer scalar emitted `z.number().int()`, which is "an integer within the safe range" and
346
+ * nothing more. Measured at the wire on `workerd`, against a document publishing the widths below:
347
+ * a `uint8` accepted `-5` and `100000`, an `int32` accepted `3000000000` and `-3000000000`.
348
+ *
349
+ * The differential oracle cannot see it. JSON Schema 2020-12 treats `format` as an annotation, so
350
+ * Ajv on the published document answers VALID for all of those and agrees with the server while
351
+ * both are wrong. Ajv says so out loud: `unknown format "uint8" ignored`.
352
+ *
353
+ * Zod 4 has the named formats for the two widths that have them, and they emit better JSON Schema
354
+ * than a bare bound would. The rest take explicit bounds.
355
+ *
356
+ * **`int64` and `uint64` keep the safe-integer ceiling, and that is a limit rather than a
357
+ * choice.** `JSON.parse` has already lost a value above 2^53 before any validator runs, so the
358
+ * alternatives are to refuse it or to accept a number that is not the one the caller sent.
359
+ * Refusing is the honest one. `z.int64()` exists but is a `bigint`, which a JSON body cannot
360
+ * carry. What changes here is only that `uint64` stops accepting negatives.
361
+ */
362
+ int8: "z.int().min(-128).max(127)",
363
+ int16: "z.int().min(-32768).max(32767)",
364
+ int32: "z.int32()",
365
+ int64: "z.int()",
366
+ safeint: "z.int()",
367
+ uint8: "z.int().min(0).max(255)",
368
+ uint16: "z.int().min(0).max(65535)",
369
+ uint32: "z.uint32()",
370
+ uint64: "z.int().min(0)",
371
+ integer: "z.int()",
352
372
  float: "z.number()",
353
373
  float32: "z.number()",
354
374
  float64: "z.number()",
@@ -859,8 +879,25 @@ function modelToZod(program, model) {
859
879
  * is a number - the disagreement is with the wire.
860
880
  *
861
881
  * Reading the TYPE answers the question the emitted text was standing in for. `SCALARS` remains the
862
- * single list of what becomes a number, so this cannot drift from what `scalarToZod` emits.
882
+ * single list of what becomes a number.
883
+ *
884
+ * **It still inspects the emitted expression to decide WHICH kind, and that did drift once more.**
885
+ * The test was `startsWith("z.number()")`, and giving each integer width its declared bounds spelled
886
+ * nine of them `z.int(`, `z.int32(` and `z.uint32(` instead. Every one silently lost its wire
887
+ * decoder: 22 arms went red at once, which is the only reason it was not shipped. The prefixes live
888
+ * in one named constant now.
889
+ *
890
+ * **That claim named a `scalars.test.ts` which did not exist, and nothing graded the list.**
891
+ * Measured: dropping `z.uint32(` from it, and separately `z.int(`, left all 468 arms in this
892
+ * package green, because no fixture had ever declared an integer width as a PARAMETER - the only
893
+ * place the decoder is attached. `test/widths/` now declares all ten as query parameters and sends
894
+ * each as text, so a spelling this list forgets fails here rather than at a consumer.
895
+ */
896
+ /**
897
+ * Every emitted expression that denotes a JSON number. Named once, because `SCALARS` and this list
898
+ * have to agree and nothing about the two being in the same file makes them.
863
899
  */
900
+ const NUMERIC_EXPRESSIONS = ["z.number()", "z.int(", "z.int32(", "z.uint32("];
864
901
  export function wireKindOf(program, type) {
865
902
  if (type.kind === "Scalar") {
866
903
  const encoded = encodedTypeOf(program, type);
@@ -870,7 +907,7 @@ export function wireKindOf(program, type) {
870
907
  const mapped = SCALARS[current.name];
871
908
  if (mapped === undefined)
872
909
  continue;
873
- if (mapped.startsWith("z.number()"))
910
+ if (NUMERIC_EXPRESSIONS.some((prefix) => mapped.startsWith(prefix)))
874
911
  return "number";
875
912
  if (mapped === "z.boolean()")
876
913
  return "boolean";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-zod",
3
- "version": "0.26.0",
3
+ "version": "0.27.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",
@@ -42,6 +42,9 @@
42
42
  "access": "public",
43
43
  "provenance": true
44
44
  },
45
+ "dependencies": {
46
+ "redos-detector": "^6.1.4"
47
+ },
45
48
  "devDependencies": {
46
49
  "@types/node": "^26.5.1",
47
50
  "@typespec/compiler": "1.16.0",
@@ -62,7 +65,7 @@
62
65
  "typespec-http-zod": "link:.",
63
66
  "vitest": "^5.0.0",
64
67
  "yaml": "^2.9.1",
65
- "zod": "^4.5.2"
68
+ "zod": "^4.6.5"
66
69
  },
67
70
  "peerDependencies": {
68
71
  "@typespec/compiler": "^1.15.0",
@@ -85,7 +88,7 @@
85
88
  },
86
89
  "tspMain": "lib/main.tsp",
87
90
  "scripts": {
88
- "build": "tsc -p tsconfig.build.json",
91
+ "build": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
89
92
  "test": "tsc -p tsconfig.build.json && vitest run",
90
93
  "typecheck": "tsc -p tsconfig.json",
91
94
  "lint": "oxlint src/ test/",