Skip to content

Add lazySchemas option for on-demand model namespaces - #105

Merged
xban1x merged 1 commit into
mainfrom
feat/lazy-schemas
Oct 22, 2026
Merged

xban1x merged 1 commit into
mainfrom
feat/lazy-schemas

Conversation

@xban1x

@xban1x xban1x commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Requested by Urban · project thread

Summary

Adds a lazySchemas option (--lazySchemas, requires tsNamespaces) that builds each model schema on first access instead of at import.

With tsNamespaces, importing a models module today constructs every Zod schema of the namespace, even when the importer needs one of them. On large APIs that is a measurable share of frontend cold start (the TOS frontend spends tens of milliseconds per details route constructing schemas it never reads). With lazySchemas: true the generator emits:

const once = <T>(create: () => T) => {
  let value: T | undefined;
  return () => (value ??= create());
};

const CategorySchema = once(() => z.object({ id: z.int().nullable(), name: z.string().nullable() }).partial());
const PetSchema = once(() => z.object({ id: z.int().nullish(), category: CategorySchema().nullish() }));

export const PetModels = {
  /** JSDoc kept per member */
  get CategorySchema() {
    return CategorySchema();
  },
  get PetSchema() {
    return PetSchema();
  },
};
export declare namespace PetModels {
  export type Category = z.infer<typeof PetModels.CategorySchema>;
  export type Pet = z.infer<typeof PetModels.PetSchema>;
}
  • PetModels.PetSchema is a regular Zod schema (.extend, .shape, .pick work) and the same instance on every access.
  • PetModels.Pet keeps resolving as a type through the merged declare namespace; circular schemas keep their explicit type and z.ZodObject<z.ZodRawShape> & z.ZodType<...> annotation.
  • Enum constants (PetModels.Status) are lazy getters too.
  • modelsInCommon proxies forward to the common namespace through getters.
  • Sibling references inside a schema become factory calls (CategorySchema()); member accesses, object keys, string and .regex(/…/) literals are left alone.
  • compileSchemas still wraps non-circular factories in z.compile(...).

Changes

  • Option plumbing: options.ts, options.const.ts, generate.command.ts, resolveConfig.ts (throws lazySchemas requires tsNamespaces), native/src/config.rs.
  • TypeScript generator: generateModels.ts (renderLazyModels, lazy proxy) and generate.lazy.utils.ts (sibling-call rewriter, once helper lines).
  • Native renderer: render.rs (render_lazy_common_models, render_lazy_schema_parts, call_lazy_schema_references, lazy render_proxy branch). Output is byte-identical to the TypeScript generator.
  • Parity matrix: lazySchemas is a 7th layout key (128 layouts, 56 invalid, 72 runtime-checked).
  • Tests: shape, compile, proxy, TS/native agreement and a runtime import test (enum, regex, circular, identity) in generateModels.test.ts; config validation in resolveConfig.test.ts.
  • README: new "Lazy schemas" section, option listed under the generate command and in generation modes.

Test plan

  • bun run build:native
  • bun run test:ts (1327 passed)
  • bun run test:native (1327 passed)
  • bun run typecheck, bun run lint:check, bun run format
  • Generated lazy output for the petstore fixture typechecks with tsc --strict and runs under bun (circular parse, enum access, .extend, shared identity)

Generated by Claude Code

With tsNamespaces enabled, importing a models module built every Zod
schema of the namespace at import time, even when the importer needed a
single one. The new lazySchemas option emits each schema as a memoized
module-level factory, exports the namespace as an object of getters and
keeps the inferred types in a merged `declare namespace`, so schemas are
built on first access. Common-model proxies forward to the shared
namespace through getters as well. Both the TypeScript generator and the
native renderer emit identical output; the option is rejected without
tsNamespaces.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH7NW9diCa2nftiQTsmp1F
@xban1x
xban1x merged commit b4375bb into main Oct 22, 2026
6 checks passed
@xban1x
xban1x deleted the feat/lazy-schemas branch October 22, 2026 16:52
urbankrepel-povio pushed a commit that referenced this pull request Oct 23, 2026
Merging main brought in #105, whose lazy-schema helpers still took the
property reference names as `&HashMap<&str, String>`. This branch replaced
that map with `PropertyRefNames` (built once per render), so the native
crate didn't compile on the PR's merge ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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