<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Jacob Ley&#39;s blog</title>
    <link>https://jacobley.github.io/blog/</link>
    <atom:link href="https://jacobley.github.io/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Benchmarks, comparisons and the stories behind the open-source projects I work on.</description>
    <language>en</language>
    <item>
      <title>One library, several languages, held to one set of test vectors</title>
      <link>https://jacobley.github.io/blog/one-library-several-languages</link>
      <guid isPermaLink="true">https://jacobley.github.io/blog/one-library-several-languages</guid>
      <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
      <description>genever, populate-files and generated-signature now have Go implementations. Each port runs expected values the TypeScript implementation computed once, including which error a bad schema fails with and where.</description>
      <content:encoded><![CDATA[<p><a href="https://jacobley.github.io/leyman/packages/genever/" target="_blank" rel="noreferrer">genever</a> generates TypeScript types from JSON Schema and OpenAPI documents. Since November 8 it also has a Go version, released as v0.1.0:</p>
<pre><code class="language-sh">go get -tool github.com/JacobLey/leyman/apps/genever/go/cmd/genever@v0.1.0
</code></pre>
<p>It reads the same schemas, has the same strict mode, and rejects the same bad schemas with the same kind of error at the same JSON Pointer. It writes Go structs instead of TypeScript types. Two of genever's dependencies came along, because the Go command has to sign its output and keep a manifest the way the TypeScript one does: <a href="https://jacobley.github.io/leyman/packages/populate-files/" target="_blank" rel="noreferrer">populate-files</a> and <a href="https://jacobley.github.io/leyman/packages/generated-signature/" target="_blank" rel="noreferrer">generated-signature</a> are Go modules now too, released at the same version.</p>
<p>This continues a pattern in my adoption of Go, which forces language-agnostic support for common functionality (in this case, type-ifying JSON Schema + OpenAPI).</p>
<p>None of the Go code is tested against the TypeScript code. Both are tested against a set of test vectors: inputs and expected outputs, as JSON, that the TypeScript implementation computed once and that are committed to the repository.</p>
<h2 id="one-directory-per-package-one-per-language" tabindex="-1">One directory per package, one per language</h2>
<p>Each of these packages is a directory named after the package, with one project per language beside the vectors they share:</p>
<pre><code>apps/genever/
├── README.md        what every implementation promises
├── ts/              published to npm as genever
├── go/              the Go module github.com/JacobLey/leyman/apps/genever/go
└── test-vectors/    schemas, sample values and error cases, as JSON
</code></pre>
<p><a href="https://github.com/JacobLey/leyman/tree/main/apps/populate-files" target="_blank" rel="noreferrer">populate-files</a> and <a href="https://github.com/JacobLey/leyman/tree/main/tools/generated-signature" target="_blank" rel="noreferrer">generated-signature</a> have the same shape. <a href="https://github.com/JacobLey/leyman/tree/main/tools/constant-consistent-hash" target="_blank" rel="noreferrer">constant-consistent-hash</a>, the oldest of the four, has <code>ts/</code>, <code>go/</code>, <code>rust/</code>, and a <code>wasm/</code> package that calls the Rust implementation from TypeScript.</p>
<p>Until the release, the TypeScript package was the root and the Go code lived in a <code>go-impl/</code> folder next to it. Moving each language into its own directory means neither is the package. The group README describes what holds in every language (the format a file is signed in, which schemas are rejected and where), and a third language has an obvious place to go.</p>
<h2 id="where-the-expected-values-come-from" tabindex="-1">Where the expected values come from</h2>
<p>genever's vectors are a <a href="https://github.com/JacobLey/leyman/tree/main/apps/genever/test-vectors" target="_blank" rel="noreferrer">TypeScript package</a> that writes JSON files under <code>out/</code> on every build:</p>
<ul>
<li>45 schema fixtures, each with sample values it accepts. The schemas are <a href="https://jacobley.github.io/leyman/packages/juniper/" target="_blank" rel="noreferrer">Juniper</a> definitions and the samples come from juniper-arbitrary with a fixed seed, so a file only changes when a fixture does.</li>
<li>Two OpenAPI documents, 3.0.3 and 3.1.0, with samples of each component.</li>
<li><code>errors.json</code>: 74 schemas genever rejects, each with the kind of error and the JSON Pointer it must name.</li>
</ul>
<p>The files are committed, and CI fails if one is stale. The TypeScript genever runs the vectors from the package; the Go one reads the JSON by relative path:</p>
<pre><code class="language-go">func testVectors() fs.FS {
	return os.DirFS(filepath.Join(&quot;..&quot;, &quot;test-vectors&quot;))
}
</code></pre>
<p>Reading by path was a late change. The vectors started as a Go module that embedded the JSON, which works until you release: a published Go module can't require one that isn't published, and the vectors are only for tests. So the Go tests read the working copy, and the Go project lists the vectors in its Nx <code>implicitDependencies</code> so its tests rerun when they change (<a href="https://github.com/JacobLey/leyman/commit/6a6fe44e" target="_blank" rel="noreferrer">commit</a>).</p>
<p>generated-signature has 187 cases in <a href="https://github.com/JacobLey/leyman/blob/main/tools/generated-signature/test-vectors/data/test-vectors.json" target="_blank" rel="noreferrer">one file</a> (signing, verifying, manual sections, manifests), and populate-files has 71 scenarios: files written, hand edits made, then one call with the results, error and files it must leave behind.</p>
<p>No implementation regenerates the values it is tested against: if the Go port could rewrite them, a change in Go would update the vectors and pass.</p>
<h2 id="the-same-error-at-the-same-pointer" tabindex="-1">The same error, at the same pointer</h2>
<p>genever's error cases are vectors too. One per kind, from <a href="https://github.com/JacobLey/leyman/blob/main/apps/genever/test-vectors/out/errors.json" target="_blank" rel="noreferrer"><code>errors.json</code></a>:</p>
<pre><code class="language-json">{
    &quot;invalid&quot;: {
        &quot;type is unknown&quot;: { &quot;schema&quot;: { &quot;type&quot;: &quot;date&quot; }, &quot;pointer&quot;: &quot;#/type&quot; }
    },
    &quot;strict&quot;: {
        &quot;unknown keyword&quot;: { &quot;schema&quot;: { &quot;type&quot;: &quot;string&quot;, &quot;foo&quot;: 1 }, &quot;pointer&quot;: &quot;#/foo&quot; }
    },
    &quot;unsupported&quot;: {
        &quot;$dynamicRef&quot;: { &quot;options&quot;: { &quot;strict&quot;: false }, &quot;schema&quot;: { &quot;$dynamicRef&quot;: &quot;#node&quot; }, &quot;pointer&quot;: &quot;#/$dynamicRef&quot; }
    }
}
</code></pre>
<p>The <a href="https://github.com/JacobLey/leyman/blob/main/apps/genever/go/errors_test.go" target="_blank" rel="noreferrer">Go test</a> checks the error's kind with <code>errors.Is</code> and its pointer, and that each strict mode case is accepted once strict mode is off:</p>
<pre><code class="language-go">var geneverErr *genever.Error
if !errors.Is(err, kind) || !errors.As(err, &amp;geneverErr) || geneverErr.Pointer != item.Pointer {
	t.Fatalf(&quot;got %v, expected %s at %s&quot;, err, kind, item.Pointer)
}
</code></pre>
<p>Moving the cases out of the TypeScript tests made those tests stricter too. They had checked the error's class but not the pointer. And the whole suite had run as OpenAPI 3.0, which hid that one check (<code>exclusiveMaximum</code>) behaved differently outside it; now only the two cases that need OpenAPI 3.0 run as it (<a href="https://github.com/JacobLey/leyman/commit/b6fd7cf3" target="_blank" rel="noreferrer">commit</a>).</p>
<h2 id="what-the-vectors-caught" tabindex="-1">What the vectors caught</h2>
<p>generated-signature's vectors cover the places where Go and JavaScript disagree by default: what JavaScript's <code>\s</code> and <code>trim()</code> count as whitespace, Node.js's <code>extname</code>, lowercase <code>İ</code>, the byte order mark <code>TextDecoder</code> drops, and <code>JSON.stringify</code>'s escaping. One case lists five manifest keys, <code>Z</code>, <code>a</code>, <code>é</code>, <code>😀</code> and <code>&#xFFFF;</code>, in that order. JavaScript compares strings by UTF-16 code unit, so the emoji (a surrogate pair starting at <code>0xD83D</code>) sorts before <code>&#xFFFF;</code>. Go compares UTF-8 bytes, where <code>&#xFFFF;</code> comes first. A manifest written by one would look edited to the other, so the Go code sorts by <code>utf16.Encode</code>.</p>
<p>In populate-files, the TypeScript package was the one that changed. The Go port decided every file before writing any, so a refused write left everything as it was. The TypeScript package wrote files in parallel as it decided them, so a refusal could come after other files were written. The scenarios couldn't pin the files a failed call leaves while the two disagreed, so the TypeScript package changed to match the port in populate-files 1.2.3 (<a href="https://github.com/JacobLey/leyman/commit/fe4dff12" target="_blank" rel="noreferrer">commit</a>), and the scenarios now pin those files.</p>
<p>A third turned up the day before release. The released modules declare <code>go 1.24</code>, the oldest Go they support, and CI now runs each released module's tests on Go 1.24 as well as the current Go. That run failed: Go 1.24's JSON decoder reports an unclosed <code>{</code> or <code>[</code> only when asked for the closing delimiter, which genever-go's order-keeping JSON parser had been discarding, so truncated JSON was accepted. It now fails with <code>ErrSyntax</code> (<a href="https://github.com/JacobLey/leyman/commit/2783a6b7" target="_blank" rel="noreferrer">commit</a>).</p>
<h2 id="where-vectors-stop" tabindex="-1">Where vectors stop</h2>
<p>The vectors say which schemas are accepted and which values every generated type must hold. They can't say how narrow a Go type is, since Go has no union or literal types and a <code>json.RawMessage</code> field would hold every sample. So the Go tests also read each fixture's generated declarations back with <code>go/types</code> and compare them with what's expected: struct fields and their types, union variants, enum constants. A fixture without expectations fails, so a new one can't go unchecked. Neither implementation's tests compare generated text, so formatting can change freely.</p>
<p>For how genever-go compares with other Go generators, the <a href="https://jacobley.github.io/leyman/packages/genever/go-comparison" target="_blank" rel="noreferrer">Go comparison</a> runs it, <a href="https://pkg.go.dev/github.com/atombender/go-jsonschema" target="_blank" rel="noreferrer">go-jsonschema</a> and <a href="https://pkg.go.dev/github.com/oapi-codegen/oapi-codegen/v2" target="_blank" rel="noreferrer">oapi-codegen</a> through the same probes every night: 18 values decoded into the type generated from a JSON Schema, and 7 into the type for an OpenAPI request body. I wrote genever-go, and only its results are tested.</p>
<p>In the run of November 11, 2026:</p>
<table tabindex="0">
<thead>
<tr>
<th>Tool</th>
<th>Version</th>
<th>JSON Schema probes</th>
<th>OpenAPI probes</th>
</tr>
</thead>
<tbody>
<tr>
<td>genever-go</td>
<td>0.1.0</td>
<td>18 / 18</td>
<td>7 / 7</td>
</tr>
<tr>
<td>go-jsonschema</td>
<td>0.24.1</td>
<td>13 / 18</td>
<td></td>
</tr>
<tr>
<td>oapi-codegen</td>
<td>2.8.2 (kin-openapi 0.152.0)</td>
<td></td>
<td>5 / 7</td>
</tr>
</tbody>
</table>
<p>go-jsonschema's type doesn't keep <code>null</code> apart from an absent property, can't hold <code>null</code> as a member of a mixed enum, and decodes three values it should reject: a tuple with a string member, a <code>oneOf</code> branch with a property of the wrong type, and a <code>patternProperties</code> value of the wrong type. oapi-codegen's type decodes the tuple with a string member and doesn't keep <code>null</code> apart from absent in a mixed enum. Before kin-openapi 0.152.0, oapi-codegen's loader rejected the 3.1 document, so it scored 0 / 7.</p>
<h2 id="try-it" tabindex="-1">Try it</h2>
<pre><code class="language-sh">go get -tool github.com/JacobLey/leyman/apps/genever/go/cmd/genever@v0.1.0
</code></pre>
<pre><code class="language-go">//go:generate go tool genever openapi.yaml --package api --signed
package api
</code></pre>
<p>It needs Go 1.24 or later, as does the code it writes. The released modules keep <code>replace</code> directives pointing at each other's workspace copies, so a change to populate-files is tested against genever before either is tagged. <code>go install</code> refuses a module with <code>replace</code> directives, which is why the install line uses <code>go get -tool</code>: it ignores a dependency's replaces and pins the version in your <code>go.mod</code>.</p>
<p>The <a href="https://jacobley.github.io/leyman/packages/genever/guides/go" target="_blank" rel="noreferrer">Go guide</a> covers the options and how Go types differ from the TypeScript ones. If a schema is accepted by one implementation and rejected by the other, that's a bug in one of them, and the <a href="https://github.com/JacobLey/leyman/tree/main/apps/genever/test-vectors" target="_blank" rel="noreferrer">test vectors</a> are where the fix starts.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How narrow are generated TypeScript types? Six JSON Schema generators compared</title>
      <link>https://jacobley.github.io/blog/json-schema-to-typescript-compared</link>
      <guid isPermaLink="true">https://jacobley.github.io/blog/json-schema-to-typescript-compared</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <description>genever, json-schema-to-typescript, quicktype, json-schema-to-zod, json-schema-to-ts and TypeBox, given the same schema every night, with each generated type checked against valid values, invalid values and reads.</description>
      <content:encoded><![CDATA[<p>A JSON Schema can say more than most generated TypeScript types do. A tuple of two numbers becomes <code>number[]</code>, an ID that must start with <code>ord_</code> becomes <code>string</code>, and an object that only allows <code>x-</code> headers takes any key. The type compiles, and bad data goes straight through it.</p>
<p>My existing package, <a href="https://jacobley.github.io/leyman/packages/juniper/" target="_blank" rel="noreferrer">Juniper</a>, avoids this by building the schema in TypeScript at runtime, so its types come straight from the code. That leaves a gap: a schema from somewhere else, like another service's OpenAPI spec or a shared JSON file, can't be <em>safely</em> pulled into TypeScript that way.</p>
<p>I wrote <a href="https://jacobley.github.io/leyman/packages/genever/" target="_blank" rel="noreferrer">genever</a> for those schemas. It reads the file and writes types narrow enough that the bad data above is a compile error. To check that it really is narrower than the alternatives, and stays that way, a nightly job gives it and five other generators the same schema and type-checks what each one produces.</p>
<h2 id="the-same-schema-two-outputs" tabindex="-1">The same schema, two outputs</h2>
<p>A schema for an order, with one property per feature that TypeScript can express but most generators don't:</p>
<pre><code class="language-json">{
  &quot;$schema&quot;: &quot;https://json-schema.org/draft/2020-12/schema&quot;,
  &quot;type&quot;: &quot;object&quot;,
  &quot;properties&quot;: {
    &quot;id&quot;: { &quot;type&quot;: &quot;string&quot;, &quot;pattern&quot;: &quot;^ord_&quot; },
    &quot;location&quot;: {
      &quot;type&quot;: &quot;array&quot;,
      &quot;prefixItems&quot;: [{ &quot;type&quot;: &quot;number&quot; }, { &quot;type&quot;: &quot;number&quot; }],
      &quot;items&quot;: false,
      &quot;minItems&quot;: 2
    },
    &quot;shipping&quot;: {
      &quot;oneOf&quot;: [
        {
          &quot;type&quot;: &quot;object&quot;,
          &quot;properties&quot;: { &quot;kind&quot;: { &quot;const&quot;: &quot;pickup&quot; }, &quot;store&quot;: { &quot;type&quot;: &quot;string&quot; } },
          &quot;required&quot;: [&quot;kind&quot;, &quot;store&quot;],
          &quot;additionalProperties&quot;: false
        },
        {
          &quot;type&quot;: &quot;object&quot;,
          &quot;properties&quot;: { &quot;kind&quot;: { &quot;const&quot;: &quot;delivery&quot; }, &quot;address&quot;: { &quot;type&quot;: &quot;string&quot; } },
          &quot;required&quot;: [&quot;kind&quot;, &quot;address&quot;],
          &quot;additionalProperties&quot;: false
        }
      ]
    },
    &quot;headers&quot;: {
      &quot;type&quot;: &quot;object&quot;,
      &quot;patternProperties&quot;: { &quot;^x-&quot;: { &quot;type&quot;: &quot;string&quot; } },
      &quot;additionalProperties&quot;: false
    },
    &quot;payment&quot;: {
      &quot;type&quot;: &quot;object&quot;,
      &quot;properties&quot;: { &quot;card&quot;: { &quot;type&quot;: &quot;string&quot; }, &quot;billingAddress&quot;: { &quot;type&quot;: &quot;string&quot; } },
      &quot;dependentRequired&quot;: { &quot;card&quot;: [&quot;billingAddress&quot;] },
      &quot;additionalProperties&quot;: false
    }
  },
  &quot;required&quot;: [&quot;id&quot;],
  &quot;additionalProperties&quot;: false
}
</code></pre>
<p><a href="https://www.npmjs.com/package/json-schema-to-typescript" target="_blank" rel="noreferrer">json-schema-to-typescript</a> 16.0.0, the most-downloaded tool that writes a <code>.ts</code> file from JSON Schema, generates this (banner comment removed):</p>
<pre><code class="language-ts">export interface Order {
  id: string;
  /**
   * @minItems 2
   */
  location?: never[];
  shipping?:
    | {
        kind: &quot;pickup&quot;;
        store: string;
      }
    | {
        kind: &quot;delivery&quot;;
        address: string;
      };
  headers?: {
    /**
     * This interface was referenced by `undefined`'s JSON-Schema definition
     * via the `patternProperty` &quot;^x-&quot;.
     */
    [k: string]: string;
  };
  payment?: {
    card?: string;
    billingAddress?: string;
  };
}
</code></pre>
<p>The <code>oneOf</code> union is right. <code>location</code> is <code>never[]</code>, so even a valid <code>[1, 2]</code> fails to compile (2020-12's <code>prefixItems</code> isn't read). <code>id</code> and <code>headers</code> take any string, and <code>payment</code> allows a card without a billing address.</p>
<p><code>npx genever order.json --name Order</code> writes:</p>
<pre><code class="language-ts">export type Order = {
    /**
     * @pattern &quot;^ord_&quot;
     */
    id: `ord_${string}`;
    /**
     * @minItems 2
     */
    location?: [number, number];
    shipping?:
        | {
              kind: 'pickup';
              store: string;
          }
        | {
              kind: 'delivery';
              address: string;
          };
    headers?: {
        [key: `x-${string}`]: string;
    };
    payment?: {
        card?: string;
        billingAddress?: string;
    } &amp; (
        | {
              card?: never;
          }
        | {
              billingAddress: string;
          }
    );
};
</code></pre>
<p>Each of those mistakes is now a compile error:</p>
<pre><code class="language-ts">const wrongId: Order = { id: 'usr_1' };
const wrongLocation: Order = { id: 'ord_1', location: [1, 2, 3] };
const wrongHeader: Order = { id: 'ord_1', headers: { accept: 'json' } };
const noBillingAddress: Order = { id: 'ord_1', payment: { card: '4242' } };
</code></pre>
<h2 id="how-the-comparison-works" tabindex="-1">How the comparison works</h2>
<p>One schema proves nothing, so the comparison runs every night in CI. Every tool gets <a href="https://github.com/JacobLey/leyman/blob/main/e2e/genever-comparison/src/documents.ts" target="_blank" rel="noreferrer">the same documents</a>: one JSON Schema with a property per feature (nullable values, mixed enums, tuples, <code>oneOf</code> objects, recursion, closed <code>patternProperties</code>, literal patterns and <code>dependentRequired</code>), and an OpenAPI 3.1 document for the tools that read OpenAPI.</p>
<p>Each <a href="https://github.com/JacobLey/leyman/blob/main/e2e/genever-comparison/src/probes.ts" target="_blank" rel="noreferrer">probe</a> is one line of TypeScript, compiled against the generated type:</p>
<ul>
<li><strong>accepts</strong>: a value the schema allows must type-check.</li>
<li><strong>rejects</strong>: a value the schema forbids must not type-check.</li>
<li><strong>reads</strong>: reading a value must give exactly the right type, compared with an identity check, so <code>any</code> doesn't pass.</li>
</ul>
<p>Most comparisons only check that valid values are accepted, which any wide enough type does; <code>any</code> passes all of them. The rejecting probes are the ones that separate the tools.</p>
<p>I wrote the probes too, so of course genever passes them. Each one follows what the spec says a validator must accept or reject, and the documents and probes are public; if one is unfair to a tool, or a feature is missing, <a href="https://github.com/JacobLey/leyman/issues" target="_blank" rel="noreferrer">open an issue</a>.</p>
<p><a href="https://www.npmjs.com/package/json-schema-to-ts" target="_blank" rel="noreferrer">json-schema-to-ts</a> and <a href="https://www.npmjs.com/package/typebox" target="_blank" rel="noreferrer">TypeBox</a> infer types inside TypeScript instead of writing a file, so they get the schema pasted in <code>as const</code>. That is their best case. Import the same schema from a <code>.json</code> file and TypeScript widens it; json-schema-to-ts then gives <code>unknown</code>.</p>
<h2 id="what-the-latest-run-found" tabindex="-1">What the latest run found</h2>
<p>Results from the 2026-11-06 run, 23 JSON Schema probes each:</p>
<table tabindex="0">
<thead>
<tr>
<th>Tool</th>
<th>Version</th>
<th>Passed</th>
<th>Missed</th>
</tr>
</thead>
<tbody>
<tr>
<td>genever</td>
<td>0.10.1</td>
<td>23</td>
<td></td>
</tr>
<tr>
<td>json-schema-to-typescript</td>
<td>16.0.0</td>
<td>18</td>
<td>tuples, closed <code>patternProperties</code>, literal patterns, <code>dependentRequired</code></td>
</tr>
<tr>
<td>quicktype</td>
<td>26.0.0</td>
<td>17</td>
<td>tuple length, narrowing <code>oneOf</code> objects, closed <code>patternProperties</code>, literal patterns, <code>dependentRequired</code></td>
</tr>
<tr>
<td>json-schema-to-ts</td>
<td>3.1.1</td>
<td>17</td>
<td>tuples, recursion (a type error), closed <code>patternProperties</code>, literal patterns, <code>dependentRequired</code></td>
</tr>
<tr>
<td>TypeBox</td>
<td>1.4.3</td>
<td>17</td>
<td>tuples, recursion, closed <code>patternProperties</code>, literal patterns, <code>dependentRequired</code></td>
</tr>
<tr>
<td>json-schema-to-zod</td>
<td>2.8.1</td>
<td>13</td>
<td>tuples, <code>oneOf</code> objects, recursion (<code>$ref</code> becomes <code>z.any()</code>), closed <code>patternProperties</code>, literal patterns, <code>dependentRequired</code></td>
</tr>
</tbody>
</table>
<p>On the OpenAPI document, genever passed 10 of 10 probes and <a href="https://www.npmjs.com/package/openapi-typescript" target="_blank" rel="noreferrer">openapi-typescript</a> 7.13.0 passed 9, missing only the literal pattern.</p>
<p>The other rows are a snapshot: what each tool did that night, at the version shown. When one of them ships a fix, the next run picks it up. Only genever's results are asserted by the comparison's tests.</p>
<p>Generating from a schema of 500 definitions took genever 54 ms, json-schema-to-typescript 141 ms and quicktype 295 ms (median of 5 runs, on a shared CI runner, so only compare within one run). The tools that infer types aren't timed, since their cost is paid while type-checking.</p>
<p>The <a href="https://jacobley.github.io/leyman/packages/genever/comparison" target="_blank" rel="noreferrer">live report</a> on genever's docs is rebuilt from each night's run, with the failed probes on hover.</p>
<h2 id="where-i-d-use-the-others" tabindex="-1">Where I'd use the others</h2>
<p>If the same schema feeds Go, Swift or Kotlin code, use quicktype: it generates for many languages and can infer a schema from sample JSON, and genever only writes TypeScript. If you want runtime checks and the schema has no <code>$ref</code>s, json-schema-to-zod gives you a Zod validator with the type attached, one generated file instead of a type plus a separate validator. json-schema-to-ts and TypeBox have no build step, which matters when the schema is already written in TypeScript; if it is, <a href="https://jacobley.github.io/leyman/packages/juniper/" target="_blank" rel="noreferrer">Juniper</a> is my version of that, with the same types genever generates. And openapi-typescript has more built around it, openapi-fetch for one; genever's <code>Paths</code> and <code>Operations</code> types are shaped like its output.</p>
<h2 id="run-it-yourself" tabindex="-1">Run it yourself</h2>
<p>The comparison is <a href="https://github.com/JacobLey/leyman/tree/main/e2e/genever-comparison" target="_blank" rel="noreferrer">a package in the repository</a>, and the nightly job runs it with <a href="https://dagger.io/" target="_blank" rel="noreferrer">Dagger</a>, so a clone, Docker and the Dagger CLI are enough to run the same thing:</p>
<pre><code class="language-sh">git clone https://github.com/JacobLey/leyman.git &amp;&amp; cd leyman
dagger call comparison-report --commit=&quot;$(git rev-parse HEAD)&quot; export --path ./reports
</code></pre>
<p>The report is <code>reports/genever.json</code>, beside the Go comparison's.</p>
<p>Versions are pinned in the lockfile, so running the same commit gives the same versions as the table.</p>
<h2 id="try-it" tabindex="-1">Try it</h2>
<pre><code class="language-sh">npx genever schema.json --name MyType
</code></pre>
<p>Commit the output, and run <code>genever --ci</code> in CI to fail when it's out of date. <a href="https://jacobley.github.io/leyman/packages/genever/getting-started" target="_blank" rel="noreferrer">Getting started</a> covers the rest, including <a href="https://jacobley.github.io/leyman/packages/genever/guides/openapi" target="_blank" rel="noreferrer">OpenAPI documents</a> and <a href="https://jacobley.github.io/leyman/packages/genever/guides/references" target="_blank" rel="noreferrer">references to other files</a>.</p>
<p>A type doesn't check anything at runtime. <code>minLength</code>, <code>format</code> and most patterns have no TypeScript equivalent, so data still needs a validator: <a href="https://ajv.js.org/" target="_blank" rel="noreferrer">Ajv</a> or <a href="https://jacobley.github.io/leyman/packages/juniper-validator/" target="_blank" rel="noreferrer">juniper-validator</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
