Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@amritk/generate-examples

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amritk/generate-examples - npm Package Compare versions

Comparing version
0.2.2
to
0.3.0
+0
-1
dist/generators/build-schema.d.ts

@@ -34,2 +34,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';

export declare const buildExampleSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>;
//# sourceMappingURL=build-schema.d.ts.map

@@ -35,2 +35,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';

export {};
//# sourceMappingURL=collect-example-imports.d.ts.map

@@ -30,2 +30,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';

export declare const generateExampleConst: (schema: JSONSchema, typeName: string, rootSchema?: Record<string, unknown>) => string;
//# sourceMappingURL=derive-example.d.ts.map
+20
-5

@@ -23,2 +23,10 @@ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';

return '1970-01-01';
case 'time':
return '00:00:00.000Z';
case 'hostname':
return 'example.com';
case 'ipv4':
return '127.0.0.1';
case 'ipv6':
return '::1';
}

@@ -74,7 +82,14 @@ }

return deriveExample(schema.anyOf[0], rootSchema, seen);
// `hasType` only matches a single string `type`; multi-type schemas fall
// through to `null`.
if (!hasType(schema))
return null;
switch (schema.type) {
if (hasType(schema))
return deriveForType(schema.type, schema, rootSchema, seen);
// Multi-type schemas (`type: ['string', 'null']`) derive from their first
// member type; `hasType` only matches a single string `type`.
if (Array.isArray(schema.type) && schema.type.length > 0) {
return deriveForType(schema.type[0], schema, rootSchema, seen);
}
return null;
};
/** Derives a canonical value for a single declared `type`. */
const deriveForType = (type, schema, rootSchema, seen) => {
switch (type) {
case 'string':

@@ -81,0 +96,0 @@ return exampleString(schema);

@@ -12,2 +12,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';

export declare const generateArbitrary: (schema: JSONSchema, typeName: string, suffix?: string) => string;
//# sourceMappingURL=generate-arbitrary.d.ts.map

@@ -24,2 +24,10 @@ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';

return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))';
case 'time':
return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(11))';
case 'hostname':
return 'fc.domain()';
case 'ipv4':
return 'fc.ipV4()';
case 'ipv6':
return 'fc.ipV6()';
}

@@ -148,6 +156,10 @@ }

return oneofExpr(schema.anyOf, suffix);
// `hasType` only matches a single string `type`; multi-type schemas
// (`type: ['string', 'null']`) fall through to the permissive fallback.
if (hasType(schema))
return scalarExpr(schema.type, schema, suffix);
// Multi-type schemas (`type: ['string', 'null']`) become a oneof over each
// member type; `hasType` only matches a single string `type`.
if (Array.isArray(schema.type)) {
const exprs = schema.type.map((type) => scalarExpr(type, schema, suffix));
return exprs.length === 1 ? exprs[0] : `fc.oneof(${exprs.join(', ')})`;
}
return 'fc.anything()';

@@ -154,0 +166,0 @@ };

@@ -43,2 +43,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';

export {};
//# sourceMappingURL=generate-files.d.ts.map

@@ -5,2 +5,1 @@ export type { GeneratedFile } from './generators/build-schema.js';

export { generateArbitrary } from './generators/generate-arbitrary.js';
//# sourceMappingURL=index.d.ts.map
{
"name": "@amritk/generate-examples",
"version": "0.2.2",
"version": "0.3.0",
"description": "Generate fast-check arbitraries and example values from JSON Schemas.",

@@ -29,4 +29,3 @@ "module": "./dist/index.js",

"files": [
"dist",
"src"
"dist"
],

@@ -46,3 +45,2 @@ "publishConfig": {

".": {
"development": "./src/index.ts",
"default": "./dist/index.js",

@@ -49,0 +47,0 @@ "types": "./dist/index.d.ts"

@@ -123,9 +123,11 @@ <div align="center">

`type` (string/number/integer/boolean/null/array/object), `properties`,
`type` — including multi-type unions like `['string', 'null']` —
(string/number/integer/boolean/null/array/object), `properties`,
`required`, `items`, `minItems`/`maxItems`, `uniqueItems`,
`minLength`/`maxLength`, `pattern`, `format` (`email`, `uuid`, `uri`/`url`,
`date`, `date-time`), `minimum`/`maximum`, `exclusiveMinimum`/`exclusiveMaximum`,
`multipleOf`, `enum`, `const`, `oneOf`/`anyOf`, `$ref`, and the `x-mjst`
extension (`Date`, `bigint`). Unsupported constructs degrade to `fc.anything()`
in arbitraries and `null` in static examples.
`date`, `date-time`, `time`, `hostname`, `ipv4`, `ipv6`), `minimum`/`maximum`,
`exclusiveMinimum`/`exclusiveMaximum`, `multipleOf`, `enum`, `const`,
`oneOf`/`anyOf`, `$ref`, and the `x-mjst` extension (`Date`, `bigint`).
Unsupported constructs degrade to `fc.anything()` in arbitraries and `null` in
static examples.

@@ -132,0 +134,0 @@ > [!TIP]

{"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../../src/generators/build-schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,eACjB,UAAU,gBACR,MAAM,0BAEnB,OAAO,CAAC,aAAa,EAAE,CAmBzB,CAAA"}
{"version":3,"file":"collect-example-imports.d.ts","sourceRoot":"","sources":["../../src/generators/collect-example-imports.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE;;GAEG;AACH,KAAK,4BAA4B,GAAG;IAClC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA0DD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,WAAY,UAAU,YAAY,4BAA4B,KAAG,MAAM,EA0BxG,CAAA"}
{"version":3,"file":"derive-example.d.ts","sourceRoot":"","sources":["../../src/generators/derive-example.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAgCjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,WAChB,UAAU,eACL,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAC9B,WAAW,CAAC,MAAM,CAAC,KACxB,OAuDF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,MAW/C,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,WACvB,UAAU,YACR,MAAM,eACH,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACnC,MAGF,CAAA"}
{"version":3,"file":"generate-arbitrary.d.ts","sourceRoot":"","sources":["../../src/generators/generate-arbitrary.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AA2JjE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,WAAY,UAAU,YAAY,MAAM,sBAAgB,MAGrF,CAAA"}
{"version":3,"file":"generate-files.d.ts","sourceRoot":"","sources":["../../src/generators/generate-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAMjE;;GAEG;AACH,KAAK,0BAA0B,GAAG;IAChC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,WACtB,UAAU,YACR,MAAM,YACN,0BAA0B,KACnC,MAsBF,CAAA"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA"}
import { describe, expect, it } from 'vitest'
import { buildExampleSchema, type GeneratedFile } from './build-schema'
/** Returns the content of a generated file, failing the test if it is absent. */
const contentOf = (files: GeneratedFile[], filename: string): string => {
const file = files.find((f) => f.filename === filename)
if (!file) throw new Error(`expected a generated file named ${filename}`)
return file.content
}
describe('buildExampleSchema', () => {
it('emits a file per schema and an index barrel', async () => {
const schema = {
type: 'object' as const,
properties: { name: { type: 'string' as const } },
required: ['name'],
}
const files = await buildExampleSchema(schema, 'User')
const names = files.map((f) => f.filename)
expect(names).toContain('user.ts')
expect(names).toContain('index.ts')
const user = contentOf(files, 'user.ts')
expect(user).toContain("import * as fc from 'fast-check'")
expect(user).toContain('export type User =')
expect(user).toContain('export const UserArbitrary: fc.Arbitrary<User> =')
expect(user).toContain('export const userExample: User =')
})
it('follows $refs into their own files and imports their arbitraries', async () => {
const schema = {
type: 'object' as const,
properties: { address: { $ref: '#/$defs/address' } },
required: ['address'],
$defs: {
address: {
type: 'object' as const,
properties: { city: { type: 'string' as const } },
required: ['city'],
},
},
}
const files = await buildExampleSchema(schema, 'User')
const names = files.map((f) => f.filename)
expect(names).toContain('address.ts')
const user = contentOf(files, 'user.ts')
expect(user).toContain("import { type Address, AddressArbitrary } from './address'")
expect(user).toContain('"address": AddressArbitrary')
// The concrete example inlines the ref's value rather than referencing a const.
expect(user).toContain('"address": { "city": "string" }')
})
it('re-exports types, arbitraries and examples from the index barrel', async () => {
const schema = { type: 'object' as const, properties: { id: { type: 'string' as const } } }
const files = await buildExampleSchema(schema, 'Thing')
const index = contentOf(files, 'index.ts')
expect(index).toContain("export { type Thing, ThingArbitrary, thingExample } from './thing';")
})
})
import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel'
import { walkRefGraph } from '@amritk/helpers/walk-ref-graph'
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
import { generateExampleFile } from './generate-files'
/**
* Represents a generated TypeScript file with its filename and content.
*/
export type GeneratedFile = {
filename: string
content: string
}
/**
* Builds all TypeScript example files from a JSON Schema by traversing all
* `$ref` / `$dynamicRef` references recursively (via the shared
* `@amritk/helpers/walk-ref-graph` walker).
*
* Each generated file exports:
* - A TypeScript type definition
* - A `fast-check` arbitrary (`FooArbitrary`) that produces schema-valid values
* - A concrete example value (`fooExample`)
*
* An `index.ts` re-exports everything. The generated output imports `fast-check`,
* which consumers must install as a (dev) dependency.
*
* @param rootSchema - The root JSON Schema to build from
* @param rootTypeName - The name for the root type (e.g. "Document")
* @param typeSuffix - Suffix appended to every `$ref`-derived name (default `''`)
* @returns An array of generated TypeScript files
*
* @example
* ```typescript
* const files = await buildExampleSchema(schema, 'Document')
* // files → [{ filename: 'document.ts', content: '...' }, { filename: 'index.ts', ... }]
* ```
*/
export const buildExampleSchema = async (
rootSchema: JSONSchema,
rootTypeName: string,
typeSuffix = '',
): Promise<GeneratedFile[]> => {
const files: GeneratedFile[] = []
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
// `index` is reserved for the barrel below, so never let a definition of
// that name overwrite it.
if (node.filename === 'index') return
const content = generateExampleFile(node.schema, node.typeName, {
rootSchema: node.rootSchema,
typeSuffix,
...(node.ref !== undefined ? { selfRef: node.ref } : {}),
})
files.push({ filename: `${node.filename}.ts`, content })
})
files.push({ filename: 'index.ts', content: generateIndexBarrel(files) })
return files
}
import { refToFilename } from '@amritk/helpers/ref-to-filename'
import { refToName } from '@amritk/helpers/ref-to-name'
import { resolveRef } from '@amritk/helpers/resolve-ref'
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from '@amritk/helpers/schema-guards'
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
/**
* Options for controlling how example imports are collected.
*/
type CollectExampleImportsOptions = {
/**
* The $ref path of the schema being generated (e.g. `#/$defs/address`).
* Prevents a file from importing itself.
*/
readonly selfRef?: string | undefined
/**
* The root schema document. URI refs that cannot be resolved within it
* are excluded from the import list (they were never generated as files).
*/
readonly rootSchema?: Record<string, unknown> | undefined
/**
* Suffix appended to every type/arbitrary name derived from a `$ref`. Must
* match the suffix used when generating the referenced files. Defaults to `''`.
*/
readonly typeSuffix?: string
}
/**
* Generates an import statement for a single $ref, importing both the generated
* type and its arbitrary from the ref's generated file.
*/
const buildImport = (ref: string, suffix: string): string => {
const filename = refToFilename(ref)
const typeName = refToName(ref, suffix)
return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}'`
}
/**
* Walks one level of the schema and yields all direct $ref strings that should
* become imports: properties, additionalProperties, items, and union branches.
*/
const collectDirectRefs = (schema: JSONSchema): string[] => {
if (typeof schema === 'boolean' || schema === null) return []
const refs: string[] = []
if (hasRef(schema)) {
refs.push(schema.$ref)
return refs
}
const propSchemas =
'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null
? Object.values(schema.properties as Record<string, JSONSchema>)
: []
for (const prop of propSchemas) {
if (hasRef(prop)) refs.push((prop as { $ref: string }).$ref)
if (hasItems(prop) && hasRef(prop.items)) refs.push((prop.items as { $ref: string }).$ref)
if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties as JSONSchema)) {
refs.push((prop.additionalProperties as { $ref: string }).$ref)
}
}
if (hasItems(schema) && hasRef(schema.items)) {
refs.push((schema.items as { $ref: string }).$ref)
}
if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties as JSONSchema)) {
refs.push((schema.additionalProperties as { $ref: string }).$ref)
}
for (const branch of [
...(hasOneOf(schema) ? schema.oneOf : []),
...(hasAnyOf(schema) ? schema.anyOf : []),
...(hasAllOf(schema) ? schema.allOf : []),
]) {
if (hasRef(branch)) refs.push((branch as { $ref: string }).$ref)
}
return refs
}
/**
* Collects import statements for all $ref dependencies of a schema. Each import
* brings in both the generated TypeScript type and the arbitrary for that ref.
*
* @example
* ```typescript
* const schema = { properties: { address: { $ref: '#/$defs/address' } } }
* collectExampleImports(schema)
* // ["import { type Address, AddressArbitrary } from './address'"]
* ```
*/
export const collectExampleImports = (schema: JSONSchema, options?: CollectExampleImportsOptions): string[] => {
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null
const rootSchema = options?.rootSchema
const typeSuffix = options?.typeSuffix ?? ''
const refs = collectDirectRefs(schema)
const seen = new Set<string>()
const imports: string[] = []
for (const ref of refs) {
const filename = refToFilename(ref)
if (seen.has(filename)) continue
if (selfFilename && filename === selfFilename) continue
// Skip refs that don't resolve in this schema (external / never generated)
if (rootSchema) {
const resolved = resolveRef(ref, rootSchema)
if (!resolved) continue
}
seen.add(filename)
imports.push(buildImport(ref, typeSuffix))
}
return imports
}
import { describe, expect, it } from 'vitest'
import { deriveExample, generateExampleConst, serializeValue } from './derive-example'
describe('deriveExample', () => {
it('prefers const, then examples, then default, then enum', () => {
expect(deriveExample({ const: 5 })).toBe(5)
expect(deriveExample({ examples: ['a', 'b'] } as never)).toBe('a')
expect(deriveExample({ default: true } as never)).toBe(true)
expect(deriveExample({ enum: ['x', 'y'] })).toBe('x')
})
it('produces canonical values per type', () => {
expect(deriveExample({ type: 'string' })).toBe('string')
expect(deriveExample({ type: 'integer' })).toBe(0)
expect(deriveExample({ type: 'number', minimum: 3 })).toBe(3)
expect(deriveExample({ type: 'boolean' })).toBe(true)
expect(deriveExample({ type: 'null' })).toBe(null)
})
it('honours string formats and length', () => {
expect(deriveExample({ type: 'string', format: 'email' })).toBe('user@example.com')
expect(deriveExample({ type: 'string', minLength: 10 })).toHaveLength(10)
})
it('builds nested objects including all declared properties', () => {
const schema = {
type: 'object' as const,
properties: { id: { type: 'string' as const }, count: { type: 'integer' as const } },
required: ['id'],
}
expect(deriveExample(schema)).toEqual({ id: 'string', count: 0 })
})
it('builds arrays honouring minItems', () => {
expect(deriveExample({ type: 'array', items: { type: 'string' }, minItems: 2 })).toEqual(['string', 'string'])
})
it('resolves $ref values against the root schema', () => {
const root = { $defs: { id: { type: 'string', const: 'abc' } } }
expect(deriveExample({ $ref: '#/$defs/id' }, root)).toBe('abc')
})
it('short-circuits recursive $refs to null', () => {
const root = {
$defs: { node: { type: 'object', properties: { next: { $ref: '#/$defs/node' } } } },
}
expect(deriveExample({ $ref: '#/$defs/node' }, root)).toEqual({ next: null })
})
})
describe('serializeValue', () => {
it('serializes bigint and Date as runtime expressions', () => {
expect(serializeValue(0n)).toBe('0n')
expect(serializeValue(new Date(0))).toBe('new Date("1970-01-01T00:00:00.000Z")')
})
it('omits undefined object properties', () => {
expect(serializeValue({ a: 1, b: undefined })).toBe('{ "a": 1 }')
})
it('serializes nested arrays and objects', () => {
expect(serializeValue({ items: [1, 2] })).toBe('{ "items": [1, 2] }')
})
})
describe('generateExampleConst', () => {
it('emits a typed const with a derived value', () => {
const schema = { type: 'object' as const, properties: { name: { type: 'string' as const } } }
expect(generateExampleConst(schema, 'Info')).toBe('export const infoExample: Info = { "name": "string" }')
})
})
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
import { resolveRef } from '@amritk/helpers/resolve-ref'
import {
hasAnyOf,
hasConst,
hasDefault,
hasEnum,
hasExamples,
hasFormat,
hasItems,
hasMaxLength,
hasMinItems,
hasMinimum,
hasMinLength,
hasOneOf,
hasProperties,
hasRef,
hasType,
isSchemaObject,
} from '@amritk/helpers/schema-guards'
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
/** Lowercases the first character of a name. e.g. "User" → "user" */
const lowerFirst = (name: string): string => name.charAt(0).toLowerCase() + name.slice(1)
/** Derives the example const name from a type name. e.g. "User" → "userExample" */
const exampleName = (typeName: string): string => `${lowerFirst(typeName)}Example`
/** Returns a representative string honouring `format` and length constraints. */
const exampleString = (schema: JSONSchema): string => {
if (hasFormat(schema)) {
switch (schema.format) {
case 'email':
return 'user@example.com'
case 'uuid':
return '00000000-0000-0000-0000-000000000000'
case 'uri':
case 'url':
return 'https://example.com'
case 'date-time':
return '1970-01-01T00:00:00.000Z'
case 'date':
return '1970-01-01'
}
}
let value = 'string'
if (hasMinLength(schema) && value.length < schema.minLength) value = value.padEnd(schema.minLength, 'x')
if (hasMaxLength(schema) && value.length > schema.maxLength) value = value.slice(0, schema.maxLength)
return value
}
/**
* Derives a single concrete, schema-valid value from a JSON Schema.
*
* Prefers explicit hints in this order: `const`, `examples[0]`, `default`,
* `enum[0]`; otherwise produces a canonical value for the declared type.
* `$ref`s are resolved and inlined by value; recursive refs short-circuit to
* `null` (tracked via `seen`).
*
* Note: values constrained only by `pattern` are not guaranteed to match the
* pattern — use the generated arbitrary when pattern fidelity matters.
*/
export const deriveExample = (
schema: JSONSchema,
rootSchema?: Record<string, unknown>,
seen: ReadonlySet<string> = new Set(),
): unknown => {
if (!isSchemaObject(schema)) return null
if (hasConst(schema)) return schema.const
if (hasExamples(schema) && Array.isArray(schema.examples) && schema.examples.length > 0) return schema.examples[0]
if (hasDefault(schema)) return schema.default
if (hasEnum(schema) && schema.enum.length > 0) return schema.enum[0]
if (hasRef(schema)) {
const ref = schema.$ref
if (seen.has(ref) || !rootSchema) return null
const resolved = resolveRef(ref, rootSchema)
if (!resolved) return null
return deriveExample(resolved as JSONSchema, rootSchema, new Set([...seen, ref]))
}
const instanceOf = getMjstInstanceOf(schema)
if (instanceOf === 'Date') return new Date(0)
const primitive = getMjstPrimitive(schema)
if (primitive === 'bigint') return 0n
if (hasOneOf(schema) && schema.oneOf[0] !== undefined) return deriveExample(schema.oneOf[0], rootSchema, seen)
if (hasAnyOf(schema) && schema.anyOf[0] !== undefined) return deriveExample(schema.anyOf[0], rootSchema, seen)
// `hasType` only matches a single string `type`; multi-type schemas fall
// through to `null`.
if (!hasType(schema)) return null
switch (schema.type) {
case 'string':
return exampleString(schema)
case 'number':
case 'integer':
return hasMinimum(schema) ? schema.minimum : 0
case 'boolean':
return true
case 'null':
return null
case 'array': {
const item = hasItems(schema) ? deriveExample(schema.items, rootSchema, seen) : null
const count = hasMinItems(schema) ? Math.max(schema.minItems, 1) : 1
return Array.from({ length: count }, () => item)
}
case 'object': {
const out: Record<string, unknown> = {}
if (hasProperties(schema)) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
out[key] = deriveExample(propSchema, rootSchema, seen)
}
}
return out
}
default:
return null
}
}
/**
* Serializes a derived value into a TypeScript source expression. Handles the
* non-JSON values `deriveExample` can produce (`Date`, `bigint`) in addition to
* plain JSON.
*/
export const serializeValue = (value: unknown): string => {
if (typeof value === 'bigint') return `${value}n`
if (value instanceof Date) return `new Date(${JSON.stringify(value.toISOString())})`
if (Array.isArray(value)) return `[${value.map(serializeValue).join(', ')}]`
if (value !== null && typeof value === 'object') {
const entries = Object.entries(value)
.filter(([, v]) => v !== undefined)
.map(([key, v]) => `${JSON.stringify(key)}: ${serializeValue(v)}`)
return `{ ${entries.join(', ')} }`
}
return JSON.stringify(value)
}
/**
* Generates an exported const holding a concrete, schema-valid example value.
*
* @example
* ```typescript
* generateExampleConst({ type: 'object', properties: { name: { type: 'string' } } }, 'Info')
* // export const infoExample: Info = { "name": "string" }
* ```
*/
export const generateExampleConst = (
schema: JSONSchema,
typeName: string,
rootSchema?: Record<string, unknown>,
): string => {
const value = deriveExample(schema, rootSchema)
return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}`
}
import { describe, expect, it } from 'vitest'
import { generateArbitrary } from './generate-arbitrary'
describe('generate-arbitrary', () => {
it('generates a record arbitrary for an object schema', () => {
const schema = {
type: 'object' as const,
properties: { name: { type: 'string' as const }, age: { type: 'integer' as const } },
required: ['name'],
}
const code = generateArbitrary(schema, 'User')
expect(code).toContain('export const UserArbitrary: fc.Arbitrary<User> =')
expect(code).toContain('fc.record(')
expect(code).toContain('"name": fc.string()')
expect(code).toContain('"age": fc.integer()')
expect(code).toContain('requiredKeys: ["name"]')
})
it('omits requiredKeys when every property is required', () => {
const schema = {
type: 'object' as const,
properties: { id: { type: 'string' as const } },
required: ['id'],
}
const code = generateArbitrary(schema, 'Doc')
expect(code).toContain('fc.record({ "id": fc.string() })')
expect(code).not.toContain('requiredKeys')
})
it('honours string length constraints', () => {
const schema = { type: 'string' as const, minLength: 2, maxLength: 8 }
expect(generateArbitrary(schema, 'Code')).toContain('fc.string({ minLength: 2, maxLength: 8 })')
})
it('maps string formats to dedicated arbitraries', () => {
expect(generateArbitrary({ type: 'string', format: 'email' }, 'E')).toContain('fc.emailAddress()')
expect(generateArbitrary({ type: 'string', format: 'uuid' }, 'U')).toContain('fc.uuid()')
expect(generateArbitrary({ type: 'string', format: 'date-time' }, 'D')).toContain(
'fc.date({ noInvalidDate: true }).map((d) => d.toISOString())',
)
})
it('uses stringMatching for pattern constraints', () => {
const schema = { type: 'string' as const, pattern: '^[a-z]+$' }
expect(generateArbitrary(schema, 'Slug')).toContain('fc.stringMatching(/^[a-z]+$/)')
})
it('honours integer range and multipleOf', () => {
const schema = { type: 'integer' as const, minimum: 0, maximum: 10, multipleOf: 2 }
const code = generateArbitrary(schema, 'Even')
expect(code).toContain('fc.integer({ min: 0, max: 10 })')
expect(code).toContain('.filter((n) => n % 2 === 0)')
})
it('adjusts exclusive integer bounds', () => {
const schema = { type: 'integer' as const, exclusiveMinimum: 0, exclusiveMaximum: 10 }
expect(generateArbitrary(schema, 'N')).toContain('fc.integer({ min: 1, max: 9 })')
})
it('uses excluded bounds for numbers', () => {
const schema = { type: 'number' as const, exclusiveMinimum: 0, maximum: 1 }
const code = generateArbitrary(schema, 'Ratio')
expect(code).toContain('min: 0, minExcluded: true')
expect(code).toContain('max: 1')
})
it('generates fc.constantFrom for enums and fc.constant for const', () => {
expect(generateArbitrary({ enum: ['a', 'b'] }, 'Choice')).toContain('fc.constantFrom("a", "b")')
expect(generateArbitrary({ const: 42 }, 'Answer')).toContain('fc.constant(42)')
})
it('references the imported arbitrary for $ref', () => {
const schema = {
type: 'object' as const,
properties: { address: { $ref: '#/$defs/address' } },
required: ['address'],
}
expect(generateArbitrary(schema, 'User')).toContain('"address": AddressArbitrary')
})
it('generates fc.array with bounds and uniqueArray for unique items', () => {
expect(generateArbitrary({ type: 'array', items: { type: 'string' }, minItems: 1 }, 'List')).toContain(
'fc.array(fc.string(), { minLength: 1 })',
)
expect(generateArbitrary({ type: 'array', items: { type: 'number' }, uniqueItems: true }, 'Set')).toContain(
'fc.uniqueArray(fc.double',
)
})
it('generates fc.oneof for unions', () => {
const schema = { oneOf: [{ type: 'string' as const }, { type: 'number' as const }] }
expect(generateArbitrary(schema, 'StringOrNumber')).toContain('fc.oneof(fc.string(), fc.double(')
})
it('maps x-mjst Date and bigint to fc.date and fc.bigInt', () => {
expect(generateArbitrary({ 'x-mjst': { instanceOf: 'Date' } } as never, 'When')).toContain('fc.date(')
expect(generateArbitrary({ 'x-mjst': { primitive: 'bigint' } } as never, 'Big')).toContain('fc.bigInt()')
})
})
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
import { refToName } from '@amritk/helpers/ref-to-name'
import {
hasAnyOf,
hasConst,
hasEnum,
hasExclusiveMaximum,
hasExclusiveMinimum,
hasFormat,
hasItems,
hasMaxItems,
hasMaximum,
hasMaxLength,
hasMinItems,
hasMinimum,
hasMinLength,
hasMultipleOf,
hasOneOf,
hasPattern,
hasProperties,
hasRef,
hasRequired,
hasType,
hasUniqueItems,
isSchemaObject,
} from '@amritk/helpers/schema-guards'
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
/**
* Derives the arbitrary const name from a type name.
* e.g. "User" → "UserArbitrary"
*/
const arbitraryName = (typeName: string): string => `${typeName}Arbitrary`
/** Builds a `fc.string({ ... })` expression honouring format and length constraints. */
const stringExpr = (schema: JSONSchema): string => {
if (hasFormat(schema)) {
switch (schema.format) {
case 'email':
return 'fc.emailAddress()'
case 'uuid':
return 'fc.uuid()'
case 'uri':
case 'url':
return 'fc.webUrl()'
case 'date-time':
return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString())'
case 'date':
return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))'
}
}
if (hasPattern(schema)) return `fc.stringMatching(/${schema.pattern}/)`
const opts: string[] = []
if (hasMinLength(schema)) opts.push(`minLength: ${schema.minLength}`)
if (hasMaxLength(schema)) opts.push(`maxLength: ${schema.maxLength}`)
return opts.length > 0 ? `fc.string({ ${opts.join(', ')} })` : 'fc.string()'
}
/** Builds a `fc.integer({ ... })` expression honouring range and multiple-of constraints. */
const integerExpr = (schema: JSONSchema): string => {
const opts: string[] = []
if (hasMinimum(schema)) opts.push(`min: ${schema.minimum}`)
else if (hasExclusiveMinimum(schema)) opts.push(`min: ${Number(schema.exclusiveMinimum) + 1}`)
if (hasMaximum(schema)) opts.push(`max: ${schema.maximum}`)
else if (hasExclusiveMaximum(schema)) opts.push(`max: ${Number(schema.exclusiveMaximum) - 1}`)
const base = opts.length > 0 ? `fc.integer({ ${opts.join(', ')} })` : 'fc.integer()'
return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base
}
/** Builds a `fc.double({ ... })` expression honouring range and multiple-of constraints. */
const numberExpr = (schema: JSONSchema): string => {
const opts: string[] = ['noNaN: true', 'noDefaultInfinity: true']
if (hasMinimum(schema)) opts.push(`min: ${schema.minimum}`)
else if (hasExclusiveMinimum(schema)) opts.push(`min: ${schema.exclusiveMinimum}`, 'minExcluded: true')
if (hasMaximum(schema)) opts.push(`max: ${schema.maximum}`)
else if (hasExclusiveMaximum(schema)) opts.push(`max: ${schema.exclusiveMaximum}`, 'maxExcluded: true')
const base = `fc.double({ ${opts.join(', ')} })`
return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base
}
/** Builds a `fc.array(...)` / `fc.uniqueArray(...)` expression for an array schema. */
const arrayExpr = (schema: JSONSchema, suffix: string): string => {
const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, suffix) : 'fc.anything()'
const opts: string[] = []
if (hasMinItems(schema)) opts.push(`minLength: ${schema.minItems}`)
if (hasMaxItems(schema)) opts.push(`maxLength: ${schema.maxItems}`)
const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? 'fc.uniqueArray' : 'fc.array'
return opts.length > 0 ? `${fn}(${items}, { ${opts.join(', ')} })` : `${fn}(${items})`
}
/** Builds a `fc.record(...)` expression for an object schema. */
const objectExpr = (schema: JSONSchema, suffix: string): string => {
if (!hasProperties(schema)) return 'fc.object()'
const required = new Set(hasRequired(schema) ? schema.required : [])
const keys = Object.keys(schema.properties)
const entries = Object.entries(schema.properties).map(
([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, suffix)}`,
)
if (entries.length === 0) return 'fc.record({})'
const model = `{ ${entries.join(', ')} }`
// fc.record treats all keys as required by default. Only emit requiredKeys
// when at least one property is optional.
if (keys.every((key) => required.has(key))) return `fc.record(${model})`
const requiredKeys = [...required].map((key) => JSON.stringify(key)).join(', ')
return `fc.record(${model}, { requiredKeys: [${requiredKeys}] })`
}
/** Builds a `fc.oneof(...)` expression from a list of branch schemas. */
const oneofExpr = (branches: readonly JSONSchema[], suffix: string): string => {
const exprs = branches.map((branch) => arbitraryExpr(branch, suffix))
return `fc.oneof(${exprs.join(', ')})`
}
/** Builds the fast-check expression for a single (non-union) JSON Schema type. */
const scalarExpr = (type: string, schema: JSONSchema, suffix: string): string => {
switch (type) {
case 'string':
return stringExpr(schema)
case 'integer':
return integerExpr(schema)
case 'number':
return numberExpr(schema)
case 'boolean':
return 'fc.boolean()'
case 'null':
return 'fc.constant(null)'
case 'array':
return arrayExpr(schema, suffix)
case 'object':
return objectExpr(schema, suffix)
default:
return 'fc.anything()'
}
}
/**
* Recursively builds the fast-check arbitrary expression for a schema node.
* `$ref`s resolve to the referenced file's exported arbitrary; everything else
* maps to the appropriate `fc.*` combinator.
*/
const arbitraryExpr = (schema: JSONSchema, suffix: string): string => {
if (!isSchemaObject(schema)) return 'fc.anything()'
if (hasRef(schema)) return arbitraryName(refToName(schema.$ref, suffix))
if (hasConst(schema)) return `fc.constant(${JSON.stringify(schema.const)})`
if (hasEnum(schema)) {
const values = (schema.enum as unknown[]).map((value) => JSON.stringify(value)).join(', ')
return `fc.constantFrom(${values})`
}
const instanceOf = getMjstInstanceOf(schema)
if (instanceOf === 'Date') return 'fc.date({ noInvalidDate: true })'
if (instanceOf) return 'fc.anything()'
const primitive = getMjstPrimitive(schema)
if (primitive === 'bigint') return 'fc.bigInt()'
if (primitive) return 'fc.anything()'
if (hasOneOf(schema)) return oneofExpr(schema.oneOf, suffix)
if (hasAnyOf(schema)) return oneofExpr(schema.anyOf, suffix)
// `hasType` only matches a single string `type`; multi-type schemas
// (`type: ['string', 'null']`) fall through to the permissive fallback.
if (hasType(schema)) return scalarExpr(schema.type, schema, suffix)
return 'fc.anything()'
}
/**
* Generates a `fast-check` arbitrary that produces schema-valid values.
*
* @example
* ```typescript
* generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
* // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "name": fc.string() })
* ```
*/
export const generateArbitrary = (schema: JSONSchema, typeName: string, suffix = ''): string => {
const expr = arbitraryExpr(schema, suffix)
return `export const ${arbitraryName(typeName)}: fc.Arbitrary<${typeName}> = ${expr}`
}
import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition'
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
import { collectExampleImports } from './collect-example-imports'
import { generateExampleConst } from './derive-example'
import { generateArbitrary } from './generate-arbitrary'
/**
* Options for controlling what gets generated in an example file.
*/
type GenerateExampleFileOptions = {
/**
* The $ref path of the schema being generated (e.g. `#/$defs/address`).
* Prevents the file from importing itself.
*/
readonly selfRef?: string
/**
* The root schema document. Used to resolve `$ref`s when deriving a concrete
* example value, and to filter out unresolvable refs from the import list.
*/
readonly rootSchema?: Record<string, unknown>
/**
* Suffix appended to every type/arbitrary name derived from a `$ref`.
* Defaults to `''` (no suffix).
*/
readonly typeSuffix?: string
}
/**
* Generates a complete TypeScript example file from a JSON Schema.
*
* The file contains:
* - An import of `fast-check` and imports for any `$ref` types and arbitraries
* - The exported TypeScript type definition
* - An exported `fast-check` arbitrary (`FooArbitrary`)
* - An exported concrete example value (`fooExample`)
*
* @example
* ```typescript
* const schema = { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] }
* generateExampleFile(schema, 'Info')
* // import * as fc from 'fast-check'
* // export type Info = { title: string }
* // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "title": fc.string() })
* // export const infoExample: Info = { "title": "string" }
* ```
*/
export const generateExampleFile = (
schema: JSONSchema,
typeName: string,
options?: GenerateExampleFileOptions,
): string => {
const typeSuffix = options?.typeSuffix ?? ''
const refImports = collectExampleImports(schema, {
selfRef: options?.selfRef,
rootSchema: options?.rootSchema,
typeSuffix,
})
const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix })
const arbitrary = generateArbitrary(schema, typeName, typeSuffix)
const example = generateExampleConst(schema, typeName, options?.rootSchema)
let result = `import * as fc from 'fast-check'\n`
for (const imp of refImports) {
result += imp + '\n'
}
result += '\n'
result += typeDefinition + '\n\n' + arbitrary + '\n\n' + example + '\n'
return result
}
export type { GeneratedFile } from './generators/build-schema'
export { buildExampleSchema } from './generators/build-schema'
export { deriveExample, generateExampleConst, serializeValue } from './generators/derive-example'
export { generateArbitrary } from './generators/generate-arbitrary'