Skip to content

[DRAFT] In house JsonSchema to TS generator - #1382

Open
JuanGalilea wants to merge 20 commits into
masterfrom
json-schema-to-ts-in-house
Open

JuanGalilea wants to merge 20 commits into
masterfrom
json-schema-to-ts-in-house

Conversation

@JuanGalilea

@JuanGalilea JuanGalilea commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

base generator and tons of testing added.

  • jsonschema to ts compiler
  • tests
  • review for unexpected regressions
  • add to actor generate-schema-types
  • improve tests to check generation better
  • add perspective or some user | actor flag
  • improve diagnostics collection
  • fix kvstore generation
  • run preprocessors before compilation
  • add --check flag

Install size change (node scripts/report-install-size.mjs)

Metric This release Latest (1.10.0) Change
Tarball download 0.36 MB 0.35 MB +4.1%
Unpacked package 1.40 MB 1.36 MB +3.1%
Full install (with dependencies) 46.28 MB 58.84 MB -21.3%

AI /show-me below

Replaces the json-schema-to-typescript dependency with an in-house schema→TypeScript compiler (src/lib/schema-to-ts/, zero runtime deps — only node:crypto), and reframes the CLI flag surface around a single --perspective.

src/lib/
├── schema-transforms.ts          # DELETED (-225): makePropertiesRequired, clearAllRequired, stripTitles…
└── schema-to-ts/                 # NEW: ~950 lines, no runtime deps
    ├── index.ts                  # narrow public surface (compile, check, normalize*, readHeader)
    ├── preprocess/               # the only Apify-aware layer
    │   ├── input.ts              #   nullable rewrite only
    │   ├── dataset.ts            #   unwrap `fields`, drop views/actorSpecification
    │   ├── kvstore.ts            #   collections → one closed lookup object
    │   └── nullable.ts           #   {type:'x', nullable:true} → {type:['x','null']}
    ├── parser.ts                 # JSON Schema → IR  + diagnostics
    ├── ir.ts                     # 9 structural node kinds, IR_VERSION = 1
    ├── emit.ts                   # IR → TS text, `type` never `interface`
    ├── canonical.ts              # sorted serialization (order-insensitive)
    ├── hash.ts                   # sha256, 16 hex chars
    ├── check.ts                  # header regex → drift detection
    ├── compile.ts                # facade: compile() / check()
    └── diagnostics.ts            # error | warning | notice codes

The pipeline

compile(schema, { types: [{name, variant}], unknownRoot? })
  jsonSchemaToIR(schema)                    parser.ts — only place IR is built
    readTypes()                             `type` is MANDATORY (no inference)
    enumToNode()                            enum ⨯ type cross-narrowing
    fromObject() → readRequired/readProps/readAdditional
    → { ir, diagnostics, notices }
  emit(ir, opts)
    header(irHash(ir, opts))                canonical(ir) → sha256 → 16 hex
    renderNode() per requested type name
  → { source, diagnostics, notices }

Two orthogonal signals come out: diagnostics = fidelity lost (something became unknown), notices = schema lint with no type impact. Neither feeds the hash.

The supplied / received split is the conceptual core — same IR, two renderings:

 renderObject(node, variant)
-  optional = !required                       // supplied: writer's obligation
+  optional = !required && !hasDefault        // received: platform materialized defaults
-  if (open) → `{...} & Record<string, unknown>`   // supplied accepts extras
+  // received is closed: unknown key = typo

Drift detection

// @generated schema-ts v1-d1bb2b62dfb769b2 — do not edit
                        │  └── sha256(canonical(IR) + options)[0..16]
                        └── IR_VERSION, compared before the hash

canonical.ts sorts props/enum members while emit.ts reproduces authored order — so reordering keys in a schema never reports drift. Header is matched anywhere in the file (gm), so a user's prepended banner is free; two headers → duplicate-header error.

CLI surface change

 apify actor generate-schema-types [path]
   -o, --output=<dir>
-  --strict            (default true)
-  --all-optional      (default false)
+  --perspective=actor|user   (default actor)

Behavior changes worth a reviewer's attention

Before After
export interface Input export type inputlowercase, from const name = 'input'
Banner with biome-ignore-all, eslint-disable, prettier-ignore-start header comment only — no lint/format suppression in generated files
Empty dataset fields → skip with warning writes export type dataset = Record<string, unknown>;
KVS: one interface per collection, concatenated one closed object, collection name → record type
prettier formatting via the library raw emitter output (4-space indent, Array<T>, parenthesized intersections)

@JuanGalilea JuanGalilea self-assigned this Sep 2, 2026
@JuanGalilea
JuanGalilea force-pushed the json-schema-to-ts-in-house branch from d212525 to e7c03f1 Compare September 8, 2026 10:11
@JuanGalilea
JuanGalilea force-pushed the json-schema-to-ts-in-house branch from 9c2467e to 1406406 Compare September 9, 2026 11:58
@JuanGalilea
JuanGalilea marked this pull request as ready for review September 9, 2026 15:14
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