feat(mock): add faker schema mock generation - #3426
Conversation
Adds `schemas` and `operationResponses` options to the faker mock generator. When `schemas: true`, orval emits a consolidated `index.faker.ts` alongside the generated schema types with one `get<SchemaName>Mock()` factory per `components/schemas` entry — useful for unit tests, Storybook stories, and seed scripts that don't need MSW handlers. Splits the Validation & Mocking docs into separate MSW and Faker pages.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds Faker generator docs, new FakerMockOptions flags, a generateFakerForSchemas API, $ref delegation to schema-level faker factories, import/writer routing for consolidated schema factories, write-pipeline integration, client filtering for operationResponses, and test fixtures/snapshots demonstrating output. ChangesFaker Schema Mock Generation Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
When `schemas: true` and `operationResponses: true` are both set on the faker generator, operation-response factories now call `get<X>Mock()` for every `#/components/schemas/X` they reference instead of inlining the body. Falls back to inlining when an operation- or tag-level override touches a property of the referenced schema, so existing override semantics keep working. Also delegates `oneOf` discriminator arms that are themselves top-level schema refs (e.g. `Dog`/`Cat`) — those now resolve to `getDogMock()`/`getCatMock()` rather than emitting per-operation helper factories.
When a schema referenced via \$ref is a string-typed enum, the
faker generator emits a runtime call (Object.values(EnumName)) for
its value. The consolidated schemas-faker file was importing such
names as type-only, producing TS1361 ('X' cannot be used as a value
because it was imported using 'import type').
Two fixes in the schemas-faker emission path:
- generateFakerForSchemas: dedupe imports by name+alias with an
"any value wins" merge so a value-position usage upgrades a
type-only push to a value import. addDependency then emits a
single `import { Foo }` line that works in both annotation and
runtime positions.
- writeFakerSchemaMocks: route value-flagged imports through the
same schemaImportPath as type imports so they reach
generateDependencyImports with the correct dependency string.
Also drops self-import of `get<Schema>Mock` when the delegation
logic references a factory defined in the same file.
Adds a tags-split repro config exercising the failure mode.
|
@melloware This PR is ready for review. I will listen to the rabbit. It allows optional schema mock data generation. In addition, if schema and operation mocks are both being generated, the operations will reuse the schema mock generator functions under-the-hood. |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
tests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/index.faker.ts (1)
77-81: 💤 Low valueConsider simplifying the array generation pattern.
The
getPetsMockfunction creates an intermediate array of numbers that are immediately discarded.Array.fromaccepts amapFnas its second parameter, so you can generate pets directly:export const getPetsMock = (): Pets => Array.from( { length: faker.number.int({ min: 1, max: 10 }) }, - (_, i) => i + 1, - ).map(() => ({ ...getPetMock() })); + () => ({ ...getPetMock() }) + );Since this is generated test code, the performance impact is negligible—this is purely a stylistic simplification.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/index.faker.ts` around lines 77 - 81, The getPetsMock function currently builds an intermediate numeric array then maps to pets; simplify by using Array.from's mapFn directly to produce pet objects without creating and discarding the index array—update getPetsMock to call Array.from with the same length from faker.number.int and provide a mapFn that returns {...getPetMock()} (keeping the return type Pets) so the implementation is more concise while preserving behavior.tests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/default/default.faker.ts (1)
20-23: 💤 Low valueConsider optimizing the nested arrayElement pattern in the generator.
The nested
faker.helpers.arrayElementcalls could be flattened tofaker.helpers.arrayElement([...Object.values(DisplayColor), undefined])for better readability and performance. Since this is generated test code validating the feature's behavior, the current pattern is acceptable, but the generator could potentially be optimized.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/default/default.faker.ts` around lines 20 - 23, The code uses a nested faker.helpers.arrayElement call for generating "color" which is unnecessary; replace the nested pattern (faker.helpers.arrayElement([ faker.helpers.arrayElement(Object.values(DisplayColor)), undefined, ])) with a single flattening call that picks from the combined list (i.e., use faker.helpers.arrayElement over an array containing ...Object.values(DisplayColor) and undefined) so the generator uses faker.helpers.arrayElement once and directly references DisplayColor values for clarity and minor perf gain.tests/__snapshots__/mock/petstore-faker-schemas/model/pet.ts (1)
12-30: 💤 Low valueConsider optimizing the generator to factor out common intersection properties.
Both union arms intersect with identical properties, creating duplication. The type could be simplified to
(Dog | Cat) & { common props }. Since this is a generated test snapshot, the current output may be intentional for validation, but the generator could potentially be optimized to produce more concise types when union arms share identical intersections.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/__snapshots__/mock/petstore-faker-schemas/model/pet.ts` around lines 12 - 30, The generated Pet type repeats the same intersection properties for both union arms (Dog & {...} and Cat & {...}); update the generator logic that builds the Pet type so it detects identical intersection property sets and factors them out into a single intersection: (Dog | Cat) & { '`@id`'?: string; id: number; name: string; tag?: string; email?: string; callingCode?: PetCallingCode; country?: PetCountry; }; locate the code that assembles union members for the Pet model (where Dog and Cat are combined) and change it to compute the shared property intersection and emit one combined type instead of duplicating the same object literal for each union arm.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/content/docs/guides/faker.mdx`:
- Around line 46-49: Update the "Response Factories" section that currently
asserts Orval always emits get<OperationId>ResponseMock factories to reflect the
new faker options: document the two new toggles named schemas and
operationResponses under the faker options table and explain that per-operation
response factory emission is conditional on operationResponses (and schema-based
generation is affected by schemas). Add a short example or note showing how to
enable/disable these options and update the same wording in the repeated block
(lines referenced 88-94) so users can discover and configure schemas and
operationResponses.
In `@packages/core/src/writers/generate-imports-for-builder.ts`:
- Around line 21-33: The schemaFactory branch currently hardcodes the dependency
path to upath.joinSafe(relativeSchemasPath, 'index.faker') which omits the
runtime file extension used by getImportExtension(); update the
schemaFactoryDeps construction (where schemaFactoryImports and schemaFactoryDeps
are defined) to append the result of getImportExtension() (same way the
per-schema branch does) to the 'index.faker' filename so the dependency passed
into from '${dependency}' includes the correct NodeNext/Node16 extension for
ESM/Node compatibility.
In `@packages/mock/src/faker/resolvers/value.ts`:
- Around line 67-77: The function shouldDelegateToSchemaFactories incorrectly
bases delegation on any faker generator in context.output.mock.generators;
change it to consult only the active faker entry for the current
resolveMockValue invocation (or enforce a single faker entry) so that delegation
respects the current entry's schemas flag and locale/options; update
shouldDelegateToSchemaFactories to accept the current generator or resolver
context (instead of scanning context.output.mock.generators), check that the
current generator has type OutputMockType.FAKER and schemas === true, and return
false otherwise; adjust call sites (e.g., resolveMockValue) to pass the active
generator or validate there that multiple faker entries are disallowed.
- Around line 99-128: hasOverrideTouchingSchema currently only checks bare
property names and regex keys, so overrides that target a referenced property by
exact-path (the "#.<path>" form) won't be detected; update
hasOverrideTouchingSchema to accept the current schema path (string) or
otherwise obtain the same path used by resolveMockOverride and, when iterating
override bucket keys, also treat keys that start with "#." as exact-path matches
using the same matching logic as resolveMockOverride (i.e., compare "#."+keyPath
=== path or the same normalization used there) before returning false so
path-targeted overrides are detected and prevent delegation to get<Schema>Mock.
In `@packages/orval/src/write-specs.ts`:
- Around line 157-163: The current logic picks the first Faker generator via the
finder assigned to fakerEntry and returns undefined unless that first entry has
schemas:true, which mismatches the resolver and generateOperations that expect
any faker entry with schemas:true; update the lookup to find the first generator
where g.type === OutputMockType.FAKER and g.schemas === true (instead of the
first FAKER regardless of schemas) so refs only delegate when an opted-in faker
is present, or alternatively enforce/validate that only one faker generator is
allowed; update the code referencing fakerEntry, OutputMockType.FAKER, schemas,
and the get<Schema>Mock()/generateOperations() workflow to use the found
opted-in entry.
- Around line 198-206: The fallback logic in write-specs.ts sets
schemaImportPath = `./${targetInfo.filename}` using a filename that has had its
extension stripped by getFileInfo(..., { extension: fileExtension }), which
yields extensionless local imports that break NodeNext/Node16; fix by importing
getImportExtension from '`@orval/core`' and append
getImportExtension(fileExtension, output.tsconfig) to schemaImportPath when
targetInfo exists (i.e., compute schemaImportPath =
`./${targetInfo.filename}${getImportExtension(fileExtension, output.tsconfig)}`)
so the generated import includes the correct extension.
---
Nitpick comments:
In
`@tests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/index.faker.ts`:
- Around line 77-81: The getPetsMock function currently builds an intermediate
numeric array then maps to pets; simplify by using Array.from's mapFn directly
to produce pet objects without creating and discarding the index array—update
getPetsMock to call Array.from with the same length from faker.number.int and
provide a mapFn that returns {...getPetMock()} (keeping the return type Pets) so
the implementation is more concise while preserving behavior.
In `@tests/__snapshots__/mock/petstore-faker-schemas/model/pet.ts`:
- Around line 12-30: The generated Pet type repeats the same intersection
properties for both union arms (Dog & {...} and Cat & {...}); update the
generator logic that builds the Pet type so it detects identical intersection
property sets and factors them out into a single intersection: (Dog | Cat) & {
'`@id`'?: string; id: number; name: string; tag?: string; email?: string;
callingCode?: PetCallingCode; country?: PetCountry; }; locate the code that
assembles union members for the Pet model (where Dog and Cat are combined) and
change it to compute the shared property intersection and emit one combined type
instead of duplicating the same object literal for each union arm.
In
`@tests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/default/default.faker.ts`:
- Around line 20-23: The code uses a nested faker.helpers.arrayElement call for
generating "color" which is unnecessary; replace the nested pattern
(faker.helpers.arrayElement([
faker.helpers.arrayElement(Object.values(DisplayColor)), undefined, ])) with a
single flattening call that picks from the combined list (i.e., use
faker.helpers.arrayElement over an array containing
...Object.values(DisplayColor) and undefined) so the generator uses
faker.helpers.arrayElement once and directly references DisplayColor values for
clarity and minor perf gain.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 70885d25-173f-44a6-85d6-9a3d7f86a4c0
📒 Files selected for processing (62)
docs/content/docs/guides/faker.mdxdocs/content/docs/guides/meta.jsondocs/content/docs/guides/msw.mdxpackages/core/src/types.tspackages/core/src/writers/generate-imports-for-builder.tspackages/core/src/writers/index.tspackages/mock/src/faker/index.tspackages/mock/src/faker/resolvers/value.tspackages/mock/src/index.tspackages/orval/src/client.tspackages/orval/src/write-specs.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/endpoints.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/cat.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/catType.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/createPetsBody.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/createPetsParams.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/createPetsSort.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/dachshund.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/dachshundBreed.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/dog.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/dogType.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/error.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/index.faker.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/index.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/labradoodle.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/labradoodleBreed.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/listPetsParams.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/listPetsSort.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/pet.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/petCallingCode.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/petCountry.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/petWithTag.tstests/__snapshots__/mock/petstore-faker-schemas-and-ops/model/pets.tstests/__snapshots__/mock/petstore-faker-schemas/endpoints.tstests/__snapshots__/mock/petstore-faker-schemas/model/cat.tstests/__snapshots__/mock/petstore-faker-schemas/model/catType.tstests/__snapshots__/mock/petstore-faker-schemas/model/createPetsBody.tstests/__snapshots__/mock/petstore-faker-schemas/model/createPetsParams.tstests/__snapshots__/mock/petstore-faker-schemas/model/createPetsSort.tstests/__snapshots__/mock/petstore-faker-schemas/model/dachshund.tstests/__snapshots__/mock/petstore-faker-schemas/model/dachshundBreed.tstests/__snapshots__/mock/petstore-faker-schemas/model/dog.tstests/__snapshots__/mock/petstore-faker-schemas/model/dogType.tstests/__snapshots__/mock/petstore-faker-schemas/model/error.tstests/__snapshots__/mock/petstore-faker-schemas/model/index.faker.tstests/__snapshots__/mock/petstore-faker-schemas/model/index.tstests/__snapshots__/mock/petstore-faker-schemas/model/labradoodle.tstests/__snapshots__/mock/petstore-faker-schemas/model/labradoodleBreed.tstests/__snapshots__/mock/petstore-faker-schemas/model/listPetsParams.tstests/__snapshots__/mock/petstore-faker-schemas/model/listPetsSort.tstests/__snapshots__/mock/petstore-faker-schemas/model/pet.tstests/__snapshots__/mock/petstore-faker-schemas/model/petCallingCode.tstests/__snapshots__/mock/petstore-faker-schemas/model/petCountry.tstests/__snapshots__/mock/petstore-faker-schemas/model/petWithTag.tstests/__snapshots__/mock/petstore-faker-schemas/model/pets.tstests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/default/default.faker.tstests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/default/default.tstests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/index.schemas.tstests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/index.tstests/__snapshots__/mock/string-enum-ref-faker-schemas-tags-split/schemas.faker.tstests/configs/mock.config.tstests/specifications/faker-schemas-string-enum-ref.yaml
- Append getImportExtension to the consolidated index.faker dependency so NodeNext/Node16 resolution gets the local-file extension. Also fix the same omission on the fallback schemaImportPath when output.schemas isn't configured. - Reuse resolveMockOverride in hasOverrideTouchingSchema so #.path-form overrides (e.g. '#.color.value') block delegation, mirroring the matching rules the rest of the faker pipeline already honors. - Tighten the fakerEntry lookup in writeFakerSchemaMocks and shouldDelegateToSchemaFactories to find the opted-in faker entry (schemas: true) directly rather than the first faker entry, so the resolver and writer stay aligned even if the duplicate-type guard in normalizeMocksOption ever loosens. - Document the new schemas and operationResponses options in the Faker guide, including a Schema Factories subsection and table rows.
Re: CodeRabbit nitpicksSkipping the 3 nitpicks from the review body — they all target output that's emitted by orval's core mock pipeline, not anything this PR introduces. Changing them would churn unrelated snapshots and belongs in a separate PR. 1. This pattern is emitted by 2. This is the generated TypeScript schema type for 3. The nested- Happy to file follow-up issues for any of these if maintainers think they're worth pursuing. |
|
We've done some initial implementation at Turo as a proof-of-concept of using the schema mocks to replace hard-coded fixtures in our tests, it works very well 🎉 |
|
@melloware Any thoughts as to when this will land in the NPM registry? Erhm, |
|
@jakiestfu I will do it tomorrow! |
Happy Memorial Day, thanks! |
Summary
Adds two options to the faker mock generator so users can opt into mock-data factories for
components/schemas, independent of operation responses:schemas(defaultfalse) — emit oneget<SchemaName>Mock()factory percomponents/schemasentry into a consolidatedindex.faker.tsnext to the generated schema types.operationResponses(defaulttrue) — toggles the existing per-operationget<OperationId>ResponseMockfactories.When both are enabled, the per-operation factories delegate to the schema-level factories instead of re-inlining schema bodies. Operation- or tag-level
override.mockrules that touch a property of the referenced schema fall back to inlining so existing override semantics keep working.Example usage
Example output
./src/api/model/index.faker.ts— one factory percomponents/schemasentry:./src/api/petstore.ts(operation-response factories, now delegating):Consumer:
Override fallback
When
override.mock.operations.<opId>.properties(or the tag-level equivalent) names a property that exists on the referenced schema, that one ref falls back to inlining so the override actually applies. The same matching rules used elsewhere in orval are honored — bare property names, regex (/pattern/), and exact path (#.foo.bar):Any matching key on the referenced schema makes
getListPetsResponseMockinline Pet's body (so the override actually lands) instead of callinggetPetMock().NodeNext / Node16 compatibility
Schema-factory imports emit with the appropriate runtime extension when
output.tsconfig.compilerOptions.moduleResolutionisNodeNextorNode16:Docs
Splits the existing "MSW" guide into separate MSW and Faker pages under Validation & Mocking, with the new options (
schemas,operationResponses) documented on the Faker page including a "Schema Factories" subsection.Bug fix included
A separate fix bundled in this PR addresses
TS1361: 'X' cannot be used as a value because it was imported using 'import type'in the consolidatedindex.faker.ts. When a schema is used both as a type and as a runtime value (e.g. viaObject.values(EnumName)for a string-typed enum component), the file now emits a unifiedimport { X }line that works in both positions, while types-only schemas continue to emitimport type { X }.Test plan
lint,typecheck, and@orval/core+orvalunit tests (1801 + 91)tests/configs/mock.config.ts— three new configs (petstoreFakerSchemas,petstoreFakerSchemasAndOps,stringEnumRefFakerSchemasTagsSplit) plustests/specifications/faker-schemas-string-enum-ref.yamlnode tests/scripts/typecheck-generated.mjs— all 15 clients passgetImportExtensionis applied to the consolidated faker dependencySummary by CodeRabbit
New Features
Documentation