Skip to content

faker js plugin#3681

Open
malcolm-kee wants to merge 2 commits intohey-api:mainfrom
malcolm-kee:feat/faker-plugin
Open

faker js plugin#3681
malcolm-kee wants to merge 2 commits intohey-api:mainfrom
malcolm-kee:feat/faker-plugin

Conversation

@malcolm-kee
Copy link
Copy Markdown
Contributor

@malcolm-kee malcolm-kee commented Apr 3, 2026

resolves #1485

What

A new faker plugin for openapi-ts that generates TypeScript factory functions using @faker-js/faker. Given an OpenAPI schema, it produces typed fake*() functions that return realistic mock data matching the schema shape.

import { faker, type Faker } from '@faker-js/faker';
import type { Pet } from '../types.gen';

export type Options = {
  faker?: Faker;
  includeOptional?: boolean | number;
  useDefault?: boolean | number;
};

export const fakePet = (options?: Options): Pet => ({
  name: ensureFaker(options).person.fullName(),
  tag: fakeTag(options),
});

Design

Architecture

Follows the established plugin pattern (same as valibot/zod): handler -> processor -> walker -> toAst -> exportAst. The handler registers a faker symbol, iterates IR schemas via plugin.forEach(), and delegates to a schema walker that dispatches each type to a converter function.

Key differences from validator plugins

  • Generates expressions, not validation schemas. The walker returns a single expression per schema node (e.g. faker.number.int()) rather than pipe chains.
  • Factory function wrapper. Each export is an arrow function (options?) => expr, not a const assignment to a schema object.
  • Dynamic options parameter. Only emitted when the factory body uses the faker instance. Literal factories (null, undefined) omit it to stay typecheck-clean.
  • Cross-plugin return types. When @hey-api/typescript is active, factories get explicit return types (: Foo) by querying typescript plugin symbols. Absent gracefully when typescript plugin isn't configured.

Factory signature

export const fakeFoo = (options?: Options): Foo => ({ ... });
  • Options type with faker (DI), includeOptional (control optional property inclusion), useDefault (use schema defaults vs faker-generated values) -- all support boolean | number for probabilistic control
  • No seed config -- the user can call faker.seed() right before calling the factory function, so there is no need to expose an option
  • The plugin generates code, not data; runtime concerns belong to the consumer

What's implemented

Core pipeline

All IR schema types handled: string, number, integer, boolean, null, unknown, void, never, undefined, const, enum (including null members), object, array, tuple, $ref.

Composition types

  • oneOf/anyOf unions with proper randomization: faker.helpers.arrayElement([() => variant1(), () => variant2()])()
  • allOf intersections merge object properties from all sub-schemas via spread: { ...fakeBase(options), ...fakeExtension(options) }
  • Discriminated unions use the discriminator property value as a literal override on each variant (spread last with as const to preserve literal types)
  • Nullable types: unions including null generate faker.datatype.boolean() ? <value> : null
  • Recursive/circular schemas handled via depth counter (_callDepth parameter) to prevent infinite loops, configurable via maxCallDepth option (default 10)

Constraints & formats

  • String formats: email, date-time, date, uuid, uri/url, ipv4, ipv6 -- mapped to corresponding faker.internet.* / faker.string.* / faker.date.* calls
  • String pattern: faker.helpers.fromRegExp(pattern)
  • String length: minLength/maxLength -> faker.string.alpha({ length: { min, max } })
  • Numeric bounds: minimum/maximum/exclusiveMinimum/exclusiveMaximum -> passed to faker.number.int/float() (exclusive bounds adjusted +/-1 for integers)
  • Array count: minItems/maxItems -> controls count in faker.helpers.multiple()
  • Priority chain: format > pattern > name inference > length constraints > fallback. Explicit schema annotations always win.

Property-name-based faker inference

Infer semantically appropriate faker helpers from property names and ancestor context, producing more realistic mock data without requiring explicit schema formats.

  • String name inference (~60 rules): email -> faker.internet.email(), firstName -> faker.person.firstName(), city -> faker.location.city(), id -> faker.string.uuid(), etc.
  • Number name inference (~6 rules): age -> faker.number.int({ min: 1, max: 120 }), price -> faker.number.float({ min: 0, max: 10000 }), port -> faker.internet.port()
  • Ancestor-aware rules: ambiguous names like name are disambiguated by parent schema -- User.name -> faker.person.fullName(), Company.name -> faker.company.name()
  • Constraint merging: rule defaults merge with schema constraints -- e.g., age with minimum: 18 produces { min: 18, max: 120 }

Optional properties & defaults

  • Optional properties: wrapped in conditional spread with includeOptional option (supports boolean | number for probabilistic inclusion)
  • Default values: useDefault option controls whether to emit schema defaults or faker-generated values (supports boolean | number)
  • Helper functions (resolveCondition, MAX_CALL_DEPTH) are emitted lazily -- only when the generated code actually needs them

Per-operation response factories

Generate faker factories for operation responses (e.g. fakePostFooResponse), enabling mock tools like MSW to produce realistic response data per endpoint.

  • Single meaningful 2XX (no errors): unsuffixed name (fakeCreatePetResponse)
  • Multiple responses or single non-2XX: suffixed per status code (fakeGetPetResponse200, fakeGetPetResponse404)
  • Wildcard codes supported (fakeCreateJobResponse2Xx)
  • Return types use indexed access when suffixed (GetPetResponses[200], GetPetErrors[404])

Per-operation request factories

Generate faker factories for operation requests (e.g. fakeUpdatePetRequest), combining body, path, query, and header parameters into one factory.

  • Single combined factory per operation
  • Only includes keys with actual data (body, path, query, headers)
  • Return type: Omit<OperationData, 'url'>

Cross-plugin API

Two consumption patterns for other plugins:

  1. Existing factories (for MSW) -- consumers use querySymbol directly with consistent metadata. All operation response factories register with role: 'response' and statusCode, so consumers always query the same way:

    plugin.querySymbol({
      category: 'schema',
      resource: 'operation',
      resourceId: operation.id,
      role: 'response',
      statusCode: '200',
      tool: 'faker',
    });
  2. Arbitrary schemas (for future plugins) -- Api methods following valibot's per-call plugin passing pattern:

    • toNode({ plugin, schema }) -- inline faker expression via walker
    • toNodeRef({ plugin, schema }) -- referenceSymbol lookup for $ref schemas, falls back to toNode

Configuration

  • locale -- generated code imports from @faker-js/faker/locale/<locale> instead of the default
  • maxCallDepth -- max recursion depth for circular schemas (default 10)
  • definitions / requests / responses -- individually toggle and customize naming/casing for each factory category
  • compatibilityVersion -- target faker v9 or v10 (auto-detected from package.json)

Possible improvements

User resolvers

Resolver types are defined in resolvers/types.ts but not wired into config or walker. Would enable user customization of faker output per schema type (string, number, array, object) via callback hooks.

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 3, 2026

Reviewed the @faker-js/faker plugin M1 implementation (review). The plugin follows established patterns well. Left 6 inline comments — the most actionable are the misleading as string cast in enum.ts and the unconditional usesFaker: true on the object visitor.

Task list (6/6 completed)
  • Read the full diff and identify major areas of change
  • Review plugin source code (config, types, walker, toAst, processor, export)
  • Review test infrastructure and snapshots
  • Check integration with existing plugin system
  • Investigate specific concerns: object usesFaker, array usesFaker, handler repetition
  • Draft and submit review

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 3, 2026

@malcolm-kee is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 3, 2026

⚠️ No Changeset found

Latest commit: c47aec7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 1.22164% with 566 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.30%. Comparing base (8d3167c) to head (c47aec7).

Files with missing lines Patch % Lines
...enapi-ts/src/plugins/@faker-js/faker/v10/walker.ts 0.79% 102 Missing and 23 partials ⚠️
...ts/src/plugins/@faker-js/faker/shared/operation.ts 0.00% 54 Missing and 14 partials ⚠️
...lugins/@faker-js/faker/shared/operation-request.ts 0.00% 41 Missing and 14 partials ⚠️
...enapi-ts/src/plugins/@faker-js/faker/v10/plugin.ts 1.85% 46 Missing and 7 partials ⚠️
...src/plugins/@faker-js/faker/v10/toAst/nameRules.ts 8.00% 34 Missing and 12 partials ⚠️
...ts/src/plugins/@faker-js/faker/v10/toAst/string.ts 0.00% 27 Missing and 8 partials ⚠️
...ts/src/plugins/@faker-js/faker/v10/toAst/number.ts 0.00% 14 Missing and 14 partials ⚠️
...pi-ts/src/plugins/@faker-js/faker/v10/processor.ts 0.00% 21 Missing and 5 partials ⚠️
...-ts/src/plugins/@faker-js/faker/v10/toAst/array.ts 0.00% 18 Missing and 7 partials ⚠️
...ts/src/plugins/@faker-js/faker/v10/toAst/object.ts 0.00% 20 Missing and 5 partials ⚠️
... and 8 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3681      +/-   ##
==========================================
- Coverage   39.39%   38.30%   -1.10%     
==========================================
  Files         520      536      +16     
  Lines       19279    19847     +568     
  Branches     5714     5879     +165     
==========================================
+ Hits         7595     7602       +7     
- Misses       9445     9885     +440     
- Partials     2239     2360     +121     
Flag Coverage Δ
unittests 38.30% <1.22%> (-1.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 3, 2026

TL;DR — New @faker-js/faker plugin that generates typed factory functions from OpenAPI schemas, supporting both Faker v9 and v10. Each schema component and operation produces exported fake*() arrow functions that use @faker-js/faker to return realistic mock data, with smart property-name inference (exact, compound, and suffix-based), format/constraint awareness, circular reference handling, request data factories, and dependency-injectable faker instances.

Key changes

  • Full plugin pipeline (handlerV10createProcessorcreateVisitor) — Implements the complete handler, processor, walker, and per-type AST converters following the established plugin architecture
  • Per-type toAst converters for all IR schema types — Handles string, number, integer, boolean, enum, object, array, tuple, $ref, union, intersection, null, unknown, void, never, and undefined with dedicated modules
  • Property-name inference via nameRules.ts — ~70 exact string rules, ~16 compound rules, ~6 suffix rules, and ~6 number rules map property names to semantically appropriate faker calls, with ancestor-aware disambiguation and suffix fallback for names like homePhone or userId
  • Format and constraint support — String formats (email, date-time, uuid, uri, ipv4, ipv6, pattern), numeric bounds, string length, and array count constraints map to corresponding faker API parameters
  • Circular reference detection with scoped depth tracking — Detects circular schemas via the transitive dependency graph and generates _callDepth-guarded factories; non-circular schemas that reference circular ones call them normally without passing depth
  • Operation request factories with Omit<*Data, 'url'> return types — Generates per-operation fake*Request functions that combine body, path params, query params, and headers into a single typed object, excluding the runtime-only url property from the return type
  • Operation response factories via irOperationToAst — Generates per-status-code response factories with smart suffixing (unsuffixed for single 2XX, suffixed when multiple responses exist)
  • compatibilityVersion config with auto-detection — Supports 9 (default) and 10 via a version-dispatching switch in handler, Api.toNode, and Api.toNodeRef; auto-detects the installed version from package.json
  • Options type with faker, includeOptional, and useDefault controls — Runtime DI for faker instances and probabilistic control over optional property inclusion and default value usage
  • Locale support — Optional locale config changes the faker import path to @faker-js/faker/locale/{locale}
  • Comprehensive documentation — Full docs page covering installation, output types, smart inference, formats, requests, responses, optional properties, defaults, circular references, locale, and custom faker instances
  • Snapshot test suite across 3 OpenAPI versions — New @test/openapi-ts-faker-v9 workspace with 4 scenarios (base, locale, typed, circular) validated against snapshot fixtures for 2.0.x, 3.0.x, and 3.1.x specs

Summary | 69 files | 2 commits | base: mainfeat/faker-plugin


Plugin handler, processor, and walker pipeline

Before: The @faker-js/faker plugin config had its handler commented out and replaced with a no-op () => {}; the naming prefix was v{{name}}.
After: The config activates the real handlerV10, renames the prefix to fake{{name}}, adds requests and responses config resolution, and wires up the full generation pipeline.

handlerV10 registers faker (value) and Faker (type) imports from @faker-js/faker, emits the shared Options type alias, computes circular schema pointers from the dependency graph, reserves slots for the resolveCondition helper and MAX_CALL_DEPTH constant, creates the processor, and subscribes to schema, parameter, requestBody, and operation events. Each event dispatches to processor.process(), which manages an emission cache, applies extractor hooks for schema splitting, and delegates to the SchemaVisitor walker. The walker dispatches each IR schema type to its corresponding toAst module and returns a FakerResult containing the expression, a usesFaker flag, and a hasCircularRef flag that controls whether the _callDepth parameter is included in the generated arrow function.

How does the resolveCondition helper work?

The handler uses a slot-based approach: it reserves a node slot at startup and only fills it after processing if any schema with a default value or optional properties was encountered. The emitted resolveCondition(condition, faker) function handles both boolean and probabilistic (0–1 number) modes, using faker.datatype.boolean({ probability }) for numeric values.

v10/plugin.ts · v10/processor.ts · v10/walker.ts · config.ts


Per-type AST converters and property-name inference

Before: No faker expression generation existed.
After: Seven toAst modules each produce a single faker API call expression, with a three-tier name-rules engine — exact match, compound (ancestor-aware) match, and suffix-based fallback — for semantic inference.

Each module is focused: string.ts handles format priority (emailfaker.internet.email(), patternfaker.helpers.fromRegExp(), length → faker.string.alpha({ length })), number.ts handles int vs float dispatch with min/max/exclusive bounds, object.ts walks properties recursively with optional-property conditional spread, array.ts uses faker.helpers.multiple() with minItems/maxItems count, enum.ts uses faker.helpers.arrayElement(), and boolean.ts emits faker.datatype.boolean().

nameRules.ts implements a data-driven approach with ~100 rules across four lookup tables:

Table Count Purpose
STRING_NAME_RULES ~70 Exact normalized name → faker call (e.g. emailfaker.internet.email())
STRING_COMPOUND_RULES ~16 Ancestor-qualified keys like company.namefaker.company.name()
STRING_SUFFIX_RULES 6 Suffix fallback — names ending in id, email, url, password, token, or phone match when no exact/compound rule hits (e.g. homePhonefaker.phone.number(), userIdfaker.string.uuid())
NUMBER_NAME_RULES ~6 Numeric names like age, price, port with default min/max bounds

Compound rules disambiguate by ancestor context — e.g., User.name resolves to faker.person.fullName() while Company.name resolves to faker.company.name(). Rule defaults merge with schema constraints so that e.g. age with minimum: 18 produces { min: 18, max: 120 }. When only one bound is specified (e.g. minLength without maxLength), the missing bound defaults to 100 to keep generated data reasonably sized.

toAst/nameRules.ts · toAst/string.ts · toAst/number.ts · toAst/object.ts


Circular reference detection and scoped depth tracking

Before: Circular schemas (self-referencing or mutually recursive) would cause infinite recursion at runtime.
After: The plugin detects circular schemas via the transitive dependency graph and generates depth-guarded factories. Non-circular schemas that reference circular ones call them without passing _callDepth, letting each circular schema start its own depth tracking from 0.

At startup, handlerV10 walks graph.transitiveDependencies to build a circularPointers set of all JSON pointers involved in cycles. This set flows through the processor and walker context alongside a per-schema isCircularSchema flag. When the walker encounters a $ref to a circular pointer, it only emits the _callDepth + 1 argument if the enclosing schema is itself circular — a Wrapper that contains a TreeNode ref simply calls fakeTreeNode(options) without depth tracking. Tuples also propagate hasCircularRef from their children so that depth guards are correctly applied to tuple elements referencing circular schemas.

Pattern Generated guard
Array<$ref> (required, in circular schema) _callDepth > MAX_CALL_DEPTH ? [] : f.helpers.multiple(...)
$ref (optional property, in circular schema) _callDepth > MAX_CALL_DEPTH || !resolveCondition(...) ? {} : { prop: ... }
$ref | null (nullable, in circular schema) _callDepth > MAX_CALL_DEPTH ? null : f.datatype.boolean() ? fakeX(...) : null
$ref (in non-circular schema) fakeX(options) — no depth parameter

New circular.yaml specs cover self-references (TreeNode), mutual recursion (Org/Member), three-way cycles (Department/Employee/Project), nullable circular refs (Comment), non-circular wrappers (Wrapper), and tuples referencing circular types (Pair).

v10/walker.ts · v10/plugin.ts · specs/3.0.x/circular.yaml · specs/3.1.x/circular.yaml


Faker v9/v10 compatibility layer

Before: The plugin targeted Faker v10 only, with direct calls to handlerV10 and toNodeV10/toNodeRefV10.
After: A compatibilityVersion config option (defaulting to 9) enables version dispatching. The installed @faker-js/faker major version is auto-detected from package.json; both v9 and v10 currently produce identical output via the shared v10 codepath.

The handler export, Api.toNode, and Api.toNodeRef each gained a switch (plugin.config.compatibilityVersion) dispatcher that routes to the v10 implementation for both versions 9 and 10. The test workspace was renamed from @test/openapi-ts-faker-v10 to @test/openapi-ts-faker-v9 with @faker-js/faker@~9.6.0 and a relaxed node >=18.0.0 engine requirement.

api.ts · config.ts · plugin.ts · types.ts


Operation request and response factories

Before: No request-level or response-level faker factories; the Api class methods (toNode, toNodeRef) returned undefined as any.
After: Per-operation request and response factories are generated. Request factories combine body, path params, query params, and headers into a single typed object with Omit<*Data, 'url'> return types that exclude the runtime-only URL property. Response factories use smart suffixing based on status codes.

operation-request.ts subscribes to the operation event, converts IR parameters to IR object schemas via irParametersToIrSchema, collects non-empty parts (body, headers, path, query), processes each independently through the walker, and assembles them into a single fake*Request factory. Return types are annotated with Omit<*Data, 'url'> when the TypeScript plugin is active — the url property is excluded because it is a runtime routing concern, not test data. The feature is gated by requests: { enabled: true } config (defaults to enabled).

operation.ts handles responses: it extracts meaningful (non-void) responses via operationResponsesMap(), and decides on suffixing — a single 2XX with no errors produces fakeCreatePetResponse, while multiple responses produce fakeGetPetResponse200 / fakeGetPetResponse404 with indexed-access return types.

What does a request factory look like?

For an updatePet operation with body, path, and query params:

export const fakeUpdatePetRequest = (options?: Options): Omit<UpdatePetData, 'url'> => {
  const f = options?.faker ?? faker;
  return {
    body: { name: f.string.sample(), ...spread },
    path: { id: f.string.uuid() },
    query: { ...spread },
  };
};

Only keys with actual data are included. Optional parameters use the resolveCondition spread pattern.

shared/operation-request.ts · shared/operation.ts · v10/plugin.ts · types.ts


Shared infrastructure and export pipeline

Before: No shared faker generation infrastructure.
After: Shared modules handle symbol registration, export statement construction, the f accessor pattern, and type definitions.

shared/export.ts builds the final export const fakeFoo = (options?) => ... statement, conditionally adding the options parameter only when the body expression uses faker, adding _callDepth when the schema is circular, and adding a return type annotation when the TypeScript plugin is active. Each function body declares const f = options?.faker ?? faker when the expression references the faker accessor, keeping the generated code concise. types.ts adds maxCallDepth config, requests and responses config, makes locale optional, and the Options type supports boolean | number for both includeOptional and useDefault.

shared/export.ts · shared/faker-expr.ts · shared/types.ts


Documentation and test coverage

Before: The faker plugin docs page was a stub with a "vote" badge; no tests existed.
After: Full documentation covering installation, output types, smart inference, formats/constraints, requests, responses, optional properties, defaults, circular references, locale, and custom faker instances. Four snapshot-based test scenarios validated across 3 OpenAPI spec versions.

The test workspace @test/openapi-ts-faker-v9 runs createClient against faker.yaml and circular.yaml specs for 2.0.x, 3.0.x, and 3.1.x, with four variants each (base, locale, typed, circular), comparing generated files against snapshot fixtures. The faker specs include operations with parameters and request bodies (listPets with query limit, createPet with body, updatePet with body + path + query, deletePet with path id) to exercise the request factory generation. The docs page covers both Faker v9 and v10 compatibility.

docs/openapi-ts/plugins/faker.md · specs/3.1.x/faker.yaml · specs/3.0.x/faker.yaml · faker/v9/test/faker.test.ts

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

Copy link
Copy Markdown

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid M1 implementation — the plugin follows established patterns well and the generated output looks clean. A few items to address, mostly around correctness of usesFaker propagation and a misleading cast.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏

ctx: FakerWalkerContext,
schema: SchemaWithType<'enum'>,
): Expression {
const members: Array<ReturnType<typeof $.literal | typeof $.fromValue>> = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type annotation Array<ReturnType<typeof $.literal | typeof $.fromValue>> resolves to a union of their return types, which works. But ReturnType<typeof $.literal | typeof $.fromValue> is a somewhat unusual pattern — if both DSL helpers share a common base type you could simplify this. Low priority / style only.

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Apr 3, 2026

@malcolm-kee I was going to ask about how you plan to handle locale and seed and I see they were removed 😬 the custom instance is a good pattern but I think we want to keep those options too – weaving the custom instance through every call will be a pain. Also, those options would basically enable static output: fakeFoo() would always produce the same thing. Yes, you could achieve it with custom instance too, but again, that requires remembering to pass the instance. Same thing with locales, people shouldn't be forced to use the instance pattern just because they don't use en-US

@malcolm-kee
Copy link
Copy Markdown
Contributor Author

I understand about locale, but seed is kinda an oddball. Because if the user is using the global fakerjs instance, they can call faker.seed(staticValue) at the top of their file before using any factory function. I don't see any value of us just include that call in our file, because technically they call it again before invoking the factory function, so we can't guarantee the output is static anyway.

However, it makes sense if we support a build time options, means we output the generated value instead of helper function, but, is there something we really want to do?

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Apr 3, 2026

I'll need to think more about seed and play around with it. My assumption was if we support locale, supporting seed would be the same pattern so no maintenance cost to us. But obviously I didn't get to play with the full experience, so it's not a final decision

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 3, 2026

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3681

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3681

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3681

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3681

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3681

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3681

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3681

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3681

commit: c47aec7

@malcolm-kee malcolm-kee marked this pull request as ready for review April 3, 2026 14:53
@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 3, 2026

Reviewed the faker plugin PR across all 58 changed files (3600+ additions). Submitted a comment-only review with 8 inline findings — mostly non-blocking robustness items around default bounds for unbounded constraints, a misleading type cast in enum handling, and a minor metadata inconsistency in operation response export. The plugin architecture faithfully follows the established pattern and the generated output is clean.

Task list (7/7 completed)
  • Read the diff TOC and identify high-risk areas
  • Review core plugin implementation (walker, toAst, processor)
  • Review API surface and types
  • Review operation response factories
  • Review config, export, and shared utilities
  • Review tests and snapshots
  • Draft and submit review

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. feature 🚀 Feature request. labels Apr 3, 2026
@malcolm-kee
Copy link
Copy Markdown
Contributor Author

@mrlubos I think it's at the right state for you to take a first look.

  • I've implemented IApi - but I'm not sure how useful it is. For MSW plugin use case, I think querySymbol is sufficient to find the fake factory.
  • I have not implemented user resolvers. But I've implemented the logic to infer faker helper based on property name, like email property would use faker.internet.email() even though the schema does not specify that.
  • locale support is added

Docs is also added.

Copy link
Copy Markdown

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured plugin that faithfully follows the established pattern (config → handler → processor → walker → toAst → export). The architecture is clean, the generated output reads naturally, and the test coverage via milestone-staged specs is thorough. A few correctness and robustness items below — nothing blocking.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏


return {
applyModifiers(result) {
return result;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createVisitor is called once per process() invocation (inside createProcessorprocessprocessor.withContext). This means a new visitor + FakerWalkerContext is created for every schema definition. The EmitTracking object is shared across all invocations (passed from the handler), which is correct. But re-creating the visitor per schema means the fakerCtx.fakerAccessor DSL node is also recreated each time — verify this doesn't cause issues with reference identity if the DSL builder caches or interns expressions.

processor: ProcessorResult;
schema: IR.SchemaObject;
statusCode: string;
tags?: ReadonlyArray<string>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In exportOperationResponse, when the group is 'errors', role is set to group (i.e., 'errors') on line 94, but then role: 'response' is used on line 115 for registerSymbol. This means the processor receives role: 'errors' but the symbol metadata uses role: 'response'. The querySymbol API consumers always query with role: 'response' + statusCode, so this works for lookup — but the processor context having role: 'errors' while the symbol says role: 'response' is inconsistent. Consider aligning them.

resource: meta.resource,
resourceId: meta.resourceId,
tool: 'typescript',
...(meta.role ? { role: meta.role } : undefined),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread syntax ...(meta.role ? { role: meta.role } : undefined) produces ...undefined when meta.role is falsy. While this is valid JS (spreading undefined is a no-op), it's a pattern that some linters flag. A cleaner alternative:

...(meta.role && { role: meta.role }),


// Check if all items are "spreadable" (produce object-like values)
const allSpreadable = schemas.every(
(s) => s.$ref || s.type === 'object' || (s.items && s.logicalOperator),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allSpreadable check includes s.items && s.logicalOperator as a spreadable condition — this catches union/intersection schemas that themselves contain items. But a union of primitives (e.g., oneOf: [string, number]) would have items and logicalOperator, pass this check, and be spread into an object literal, which wouldn't make sense. In practice this may not happen because allOf with primitive unions is unusual, but worth validating.

@mrlubos mrlubos mentioned this pull request Apr 3, 2026
Comment on lines +52 to +65
.$if(
final.usesAccessor,
(f) => {
const fakerPackagePath = plugin.config.locale
? `@faker-js/faker/locale/${plugin.config.locale}`
: '@faker-js/faker';
const fakerSymbol = plugin.external(`${fakerPackagePath}.faker`);
const fDecl = $.const('f').assign(
$.binary($('options').attr('faker').optional(), '??', $(fakerSymbol)),
);
return f.do(fDecl, $.return(final.expression));
},
(f) => f.do($.return(final.expression)),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 14-line .$if(usesAccessor, ...) block is duplicated verbatim in operation.ts at lines 146–159 and 238–251. Consider extracting a shared helper, e.g.:

function buildAccessorBody(
  plugin: FakerJsFakerPlugin['Instance'],
  result: FakerResult,
): (f: FuncBuilder) => FuncBuilder {
  // ...
}

This would reduce each call site to a single .$if(result.usesAccessor, buildAccessorBody(plugin, result), ...).

@malcolm-kee
Copy link
Copy Markdown
Contributor Author

I'm working on handling circular reference, which would becomes a problem when includeOptional: true (default).

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Apr 3, 2026

@malcolm-kee This is solved in validators, you can look there for inspiration and/or improve those too if you find a cleaner approach

@malcolm-kee
Copy link
Copy Markdown
Contributor Author

malcolm-kee commented Apr 4, 2026

@malcolm-kee This is solved in validators, you can look there for inspiration and/or improve those too if you find a cleaner approach

For this plugin it's more tricky than the validator.

Zod/validbot needs to handle circular reference because you can't declare a schema that depends on itself, so on circular reference, the declaration syntax switch to lazy().

For this plugin, the circular dependency issue is causing the faker function calling itself, causing call stack overflow. So we have to keep track of call stack depth, and then opt out when the call stack is too deep.

The circular dependency is computed upfront from the graph in the context. And then during the walk, we check if $ref is circular - only then those factories get the _callDepth parameter and depths guards. Doing this so factory function without circular schemas kept clean.

@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 4, 2026

Acknowledged the author's feedback on the circular-ref depth guard comment — the current nullable-only guard is correct since fully-required circular schemas are unfulfillable by definition. No code changes needed.

Task list (1/2 completed)
  • Review and respond to malcolm-kee's feedback on depth guard comment
  • Report progress

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 4, 2026

Confirmed the author's fix for the _callDepth latent bug in walker.ts:213 — gating isCircularRef on fakerCtx.isCircularSchema is correct. Replied and resolved the thread.

Task list (2/3 completed)
  • Review the author's fix for the _callDepth latent bug
  • Reply to the review thread and resolve it
  • Report progress

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 4, 2026

No action needed — the single review thread was already addressed by the PR author and resolved. The tuple handler in walker.ts now correctly propagates hasCircularRef from child results, consistent with the other composite type handlers.

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@malcolm-kee
Copy link
Copy Markdown
Contributor Author

Generate request data too. I think there is values to it - people might want to use them to test their server.

@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 4, 2026

Re-reviewed the new commit (40b741f4b375b7). The change wraps request factory return types with Omit<DataType, 'url'>, which resolves the type-checking issue flagged in the previous review — *Data types always include a required url literal that the factories never produce. Implementation is clean (single-line DSL change in operation-request.ts:158-159) and all 15 snapshot updates are consistent. No new issues.

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@malcolm-kee
Copy link
Copy Markdown
Contributor Author

@mrlubos faker 10 needs certain nodejs version (>22.13+) - I assume we had those different node build for compatibility check, so what should we do in this case? In there any library that we support encounters something like this before?

@malcolm-kee
Copy link
Copy Markdown
Contributor Author

I've installed faker v9 instead for our test setup. There is no breaking change at all and we're not using any faker v10 only feature, so let's not sweat it.

@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 4, 2026

Re-reviewed the new commits (4b375b7..d8842ce). The compatibilityVersion config with auto-detection, version routing scaffolding, v9 test suite rename, and docs expansion all look clean. Prior feedback on maxLength/maxItems defaults (1000 → 100) and the Omit<Data, 'url'> return type has been addressed. No new issues found.

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

@mrlubos
Copy link
Copy Markdown
Member

mrlubos commented Apr 4, 2026

@malcolm-kee I'll probably bump to Node 22+. Node 20 is near its EOL date now

Copy link
Copy Markdown

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found. This is a substantial rewrite that squashes prior commits into a single clean implementation and addresses all previous review feedback.

Changes since last review

TL;DR: The PR was force-pushed as a single squashed commit that consolidates the v1/v10 split into just v10, adds operation request factories (M5), and addresses all prior review comments.

Area Change
v1/ directory Removed — all codegen consolidated into v10/
operation-request.ts New: generates per-operation request factories with Omit<Data, 'url'> return types
config.ts / types.ts Added compatibilityVersion, maxCallDepth, requests, responses config options
enum.ts Cast fixed: as stringas string | number
walker.ts LITERAL_FALSEFALLBACK_UNDEFINED; circular ref gated on isCircularSchema; tuple now propagates hasCircularRef
object.ts Now propagates usesAccessor and hasCircularRef from children
specs/ New circular.yaml for 2.0.x, 3.0.x, 3.1.x
Zod snapshots Updated from the new circular specs
Docs Comprehensive faker.md documentation

Prior review feedback

  • as string cast in enum (reviews 1 & 2)
  • LITERAL_FALSE naming (review 1)
  • Circular ref gated on isCircularSchema (review 3)
  • Tuple hasCircularRef propagation (review 3)
  • Omit<Data, 'url'> for request factories (review 4)
  • Object usesAccessor propagation from children (review 1)

Pullfrog  | View workflow run𝕏

Copy link
Copy Markdown

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The 'id' suffix rule is very short and will produce false positives for common English words used as string property names.

Changes since last review

TL;DR: Adds suffix-based name matching as a fallback after exact/compound rules, plus a homePhone test property to validate it.

Area Change
nameRules.ts New STRING_SUFFIX_RULES array + suffix fallback in stringNameToExpression
specs/*.yaml Added homePhone: string property to Person schema across 2.0.x, 3.0.x, 3.1.x
Snapshots Updated to reflect homePhonef.phone.number() via suffix matching

No prior review comments to address — previous review found no issues.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏

// Order matters: first match wins. Suffixes are matched against normalized names.
// ---------------------------------------------------------------------------
const STRING_SUFFIX_RULES: Array<{ rule: NameRule; suffix: string }> = [
{ rule: { fakerPath: ['string', 'uuid'] }, suffix: 'id' },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suffix 'id' is only two characters. After normalization (validvalid, paidpaid, gridgrid, avoidavoid), any string property whose name ends in id would be mapped to string.uuid(). Consider using a longer suffix like 'Id'-aware matching (e.g. requiring a word boundary before id) or at minimum a 3+ character suffix such as uid or _id. Alternatively, a regex like /(?:^|[a-z])id$/ on the raw (un-normalized) name could catch userId, order_id, orderId without matching valid or paid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 Feature request. size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Faker plugin

3 participants