Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: bcherny/json-schema-to-typescript
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: haggholm/json-schema-to-typescript
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Sep 5, 2019

  1. Copy the full SHA
    7290a54 View commit details

Commits on Sep 23, 2019

  1. merge

    Petter Häggholm committed Sep 23, 2019
    Copy the full SHA
    8298f56 View commit details

Commits on Dec 20, 2019

  1. Copy the full SHA
    25a86c3 View commit details
  2. Copy the full SHA
    355b946 View commit details
  3. Merge pull request #1 from forivall/feat/enum-ref

    feat: allow referencing named enums
    haggholm authored Dec 20, 2019
    Copy the full SHA
    1b1e813 View commit details
  4. 8.0.1

    haggholm committed Dec 20, 2019
    Copy the full SHA
    9457eb4 View commit details
  5. refactor: clean up enum ref generation

    There was code generation hidden inside of the parser
    forivall committed Dec 20, 2019
    Copy the full SHA
    142c23d View commit details

Commits on Dec 23, 2019

  1. Merge pull request #2 from forivall/refactor/enum-ref-cleanup

    refactor: clean up enum ref generation
    haggholm authored Dec 23, 2019
    Copy the full SHA
    578184e View commit details

Commits on Mar 24, 2020

  1. Copy the full SHA
    71d14b1 View commit details
  2. 8.0.2

    haggholm committed Mar 24, 2020
    Copy the full SHA
    63a5b26 View commit details

Commits on Jun 17, 2020

  1. Copy the full SHA
    b38f544 View commit details
  2. Copy the full SHA
    7b0be79 View commit details
  3. fix: tests

    haggholm committed Jun 17, 2020
    Copy the full SHA
    41b4019 View commit details

Commits on Jun 22, 2020

  1. Copy the full SHA
    99f1f93 View commit details

Commits on Oct 7, 2020

  1. Copy the full SHA
    2da8116 View commit details
  2. Copy the full SHA
    b009aa5 View commit details
  3. Copy the full SHA
    6b9d38a View commit details

Commits on Oct 8, 2020

  1. feat: support propertyNames

    forivall committed Oct 8, 2020
    Copy the full SHA
    f49c127 View commit details
  2. fix: improve error handling

    forivall committed Oct 8, 2020
    Copy the full SHA
    5de1dde View commit details

Commits on Oct 9, 2020

  1. Merge pull request #5 from forivall/jsonschema6

    Support JSONSchema draft v6 in typings, and using enums for propertyNames
    haggholm authored Oct 9, 2020
    Copy the full SHA
    64ee33a View commit details

Commits on Jan 28, 2021

  1. Copy the full SHA
    c22a411 View commit details
  2. Copy the full SHA
    7d8e374 View commit details
  3. Copy the full SHA
    109a90b View commit details
  4. Copy the full SHA
    bc6d996 View commit details

Commits on Feb 8, 2021

  1. Merge pull request #7 from forivall/allof-extend

    feat: allow using allOf to create interfaces with extends
    haggholm authored Feb 8, 2021
    Copy the full SHA
    e328701 View commit details

Commits on Apr 16, 2021

  1. Merge pull request #3 from forivall/feat/circular

    feat: support circular schemas
    haggholm authored Apr 16, 2021
    Copy the full SHA
    f5373bc View commit details

Commits on Apr 19, 2021

  1. Copy the full SHA
    a82d93f View commit details
  2. Copy the full SHA
    764cc3d View commit details
  3. Merge pull request #8 from forivall/merge-upstream

    Merge upstream
    haggholm authored Apr 19, 2021
    Copy the full SHA
    95fbd98 View commit details
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.3.0
lts/erbium
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "json-schema-to-typescript",
"name": "@haggholm/json-schema-to-typescript",
"version": "10.1.4",
"description": "compile json schema to typescript typings",
"main": "dist/src/index.js",
@@ -13,7 +13,7 @@
"scripts": {
"build": "npm run lint && npm run clean && npm run build:browser && npm run build:server",
"build:browser": "browserify src/index.ts -s jstt -p tsify > dist/bundle.js",
"build:server": "tsc -d",
"build:server": "tsc -d && shx chmod +x ./dist/src/cli.js",
"clean": "shx rm -rf dist && mkdir dist",
"lint": "eslint src/*.ts test/*.ts",
"tdd": "concurrently -r -p '' -k 'npm run watch' 'npm run watch:test'",
@@ -24,9 +24,13 @@
"watch": "tsc -w",
"watch:test": "ava -w"
},
"publishConfig": {
"registry": "https://registry.npmjs.com",
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bcherny/json-schema-to-typescript.git"
"url": "git+https://github.com/haggholm/json-schema-to-typescript.git"
},
"keywords": [
"json",
@@ -46,6 +50,7 @@
},
"homepage": "https://github.com/bcherny/json-schema-to-typescript#readme",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.1",
"@types/json-schema": "^7.0.6",
"@types/lodash": "^4.14.168",
"@types/prettier": "^2.1.5",
@@ -54,7 +59,6 @@
"glob": "^7.1.6",
"glob-promise": "^3.4.0",
"is-glob": "^4.0.1",
"json-schema-ref-parser": "^9.0.6",
"json-stringify-safe": "^5.0.1",
"lodash": "^4.17.20",
"minimist": "^1.2.5",
23 changes: 22 additions & 1 deletion src/cli.ts
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
#!/usr/bin/env node

try {
const sms = require('source-map-support')
sms.install()
} catch {}

import minimist = require('minimist')
import getStdin from 'get-stdin'
import {readFile, writeFile, existsSync, lstatSync, readdirSync} from 'mz/fs'
import * as mkdirp from 'mkdirp'
import glob from 'glob-promise'
import isGlob = require('is-glob')
import * as _ from 'lodash'
import {join, resolve, dirname, basename} from 'path'
import {compile, Options} from './index'
import {pathTransform, error} from './utils'
@@ -15,7 +21,8 @@ main(
alias: {
help: ['h'],
input: ['i'],
output: ['o']
output: ['o'],
verbose: ['v']
}
})
)
@@ -26,9 +33,23 @@ async function main(argv: minimist.ParsedArgs) {
process.exit(0)
}

if (argv.verbose) {
process.env.VERBOSE = '1'
}

const argIn: string = argv._[0] || argv.input
const argOut: string | undefined = argv._[1] || argv.output // the output can be omitted so this can be undefined

function coerceBool(arg: string) {
const value = _.get(argv, arg)
const coerced = value === 'true' ? true : value === 'false' ? false : undefined
if (coerced !== undefined) {
_.set(argv, arg, coerced)
}
}

coerceBool('$refOptions.dereference.circular')

const ISGLOB = isGlob(argIn)
const ISDIR = isDir(argIn)

61 changes: 44 additions & 17 deletions src/generator.ts
Original file line number Diff line number Diff line change
@@ -12,9 +12,15 @@ import {
TIntersection,
TNamedInterface,
TUnion,
T_UNKNOWN
T_UNKNOWN,
TTypeReference
} from './types/AST'
import {log, toSafeString} from './utils'
import {INDEX_KEY_NAME, log, error, toSafeString} from './utils'

const unreachableCase = (value: never) => {
error('Unreachable Case!', value)
return new Error('Unreachable Case!')
}

export function generate(ast: AST, options = DEFAULT_OPTIONS): string {
return (
@@ -74,7 +80,7 @@ function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string,
type = [
hasStandaloneName(ast) &&
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
generateStandaloneInterface(ast, options),
(ast.paramsKeyType ? generateStandaloneType(ast, options) : generateStandaloneInterface(ast, options)),
getSuperTypesAndParams(ast)
.map(ast => declareNamedInterfaces(ast, options, rootASTName, processed))
.filter(Boolean)
@@ -159,7 +165,7 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
function generateType(ast: AST, options: Options): string {
const type = generateRawType(ast, options)

if (options.strictIndexSignatures && ast.keyName === '[k: string]') {
if (options.strictIndexSignatures && ast.keyName === INDEX_KEY_NAME) {
return `${type} | undefined`
}

@@ -175,12 +181,11 @@ function generateRawType(ast: AST, options: Options): string {

switch (ast.type) {
case 'ANY':
return 'any'
case 'ARRAY':
return (() => {
const type = generateType(ast.params, options)
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]'
})()
return options.unknownAny ? 'unknown' : 'any'
case 'ARRAY': {
const type = generateType(ast.params, options)
return type.endsWith('"') ? '(' + type + ')[]' : type + '[]'
}
case 'BOOLEAN':
return 'boolean'
case 'INTERFACE':
@@ -189,6 +194,8 @@ function generateRawType(ast: AST, options: Options): string {
return generateSetOperation(ast, options)
case 'LITERAL':
return JSON.stringify(ast.params)
case 'NEVER':
return 'never'
case 'NUMBER':
return 'number'
case 'NULL':
@@ -283,6 +290,11 @@ function generateRawType(ast: AST, options: Options): string {
return 'unknown'
case 'CUSTOM_TYPE':
return ast.params
case 'TYPE_REFERENCE':
return generateEnumReference(ast)
default:
error('Standalone name ("title") required for item', ast)
throw unreachableCase(ast)
}
}

@@ -295,11 +307,12 @@ function generateSetOperation(ast: TIntersection | TUnion, options: Options): st
return members.length === 1 ? members[0] : '(' + members.join(' ' + separator + ' ') + ')'
}

function generateInterface(ast: TInterface, options: Options): string {
function generateInterface(interfaceAst: TInterface, options: Options): string {
const genericValues = interfaceAst.tsGenericValues || {}
return (
`{` +
'\n' +
ast.params
interfaceAst.params
.filter(_ => !_.isPatternProperty && !_.isUnreachableDefinition)
.map(
({isRequired, keyName, ast}) =>
@@ -308,10 +321,11 @@ function generateInterface(ast: TInterface, options: Options): string {
.map(
([isRequired, keyName, ast, type]) =>
(hasComment(ast) && !ast.standaloneName ? generateComment(ast.comment) + '\n' : '') +
escapeKeyName(keyName) +
escapeKeyName(keyName, interfaceAst.paramsKeyType) +
(isRequired ? '' : '?') +
': ' +
(hasStandaloneName(ast) ? toSafeString(type) : type)
(hasStandaloneName(ast) ? toSafeString(type) : type) +
(genericValues[keyName] ? `<${genericValues[keyName].join(', ')}>` : '')
)
.join('\n') +
'\n' +
@@ -336,10 +350,16 @@ function generateStandaloneEnum(ast: TEnum, options: Options): string {
)
}

function generateEnumReference(ast: TTypeReference): string {
const [parent, key] = ast.params
return `${toSafeString(parent.standaloneName)}.${key.keyName}`
}

function generateStandaloneInterface(ast: TNamedInterface, options: Options): string {
return (
(hasComment(ast) ? generateComment(ast.comment) + '\n' : '') +
`export interface ${toSafeString(ast.standaloneName)} ` +
(ast.tsGenericParams ? `<${ast.tsGenericParams.join(', ')}>` : '') +
(ast.superTypes.length > 0
? `extends ${ast.superTypes.map(superType => toSafeString(superType.standaloneName)).join(', ')} `
: '') +
@@ -357,16 +377,23 @@ function generateStandaloneType(ast: ASTWithStandaloneName, options: Options): s
)
}

function escapeKeyName(keyName: string): string {
function escapeKeyName(keyName: string, hasKeyType?: object | boolean): string {
if (hasKeyType) {
return keyName
}
if (keyName.length && /[A-Za-z_$]/.test(keyName.charAt(0)) && /^[\w$]+$/.test(keyName)) {
return keyName
}
if (keyName === '[k: string]') {
if (keyName === INDEX_KEY_NAME) {
return keyName
}
return JSON.stringify(keyName)
}

function getSuperTypesAndParams(ast: TInterface): AST[] {
return ast.params.map(param => param.ast).concat(ast.superTypes)
const asts = ast.params.map(param => param.ast).concat(ast.superTypes)
if (ast.paramsKeyType) {
asts.push(ast.paramsKeyType)
}
return asts
}
10 changes: 7 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {readFileSync} from 'fs'
import {JSONSchema4} from 'json-schema'
import {Options as $RefOptions} from 'json-schema-ref-parser'
import {JSONSchema4, JSONSchema6} from 'json-schema'
import {Options as $RefOptions} from '@apidevtools/json-schema-ref-parser'
import {endsWith, merge} from 'lodash'
import {dirname} from 'path'
import {Options as PrettierOptions} from 'prettier'
@@ -109,7 +109,11 @@ export function compileFromFile(filename: string, options: Partial<Options> = DE
return compile(schema, stripExtension(filename), {cwd: dirname(filename), ...options})
}

export async function compile(schema: JSONSchema4, name: string, options: Partial<Options> = {}): Promise<string> {
export async function compile(
schema: JSONSchema4 | JSONSchema6,
name: string,
options: Partial<Options> = {}
): Promise<string> {
const _options = merge({}, DEFAULT_OPTIONS, options)

const start = Date.now()
2 changes: 1 addition & 1 deletion src/normalizer.ts
Original file line number Diff line number Diff line change
@@ -47,7 +47,7 @@ rules.set('Transform `required`=false to `required`=[]', schema => {
// TODO: default to empty schema (as per spec) instead
rules.set('Default additionalProperties to true', schema => {
if (isObjectType(schema) && !('additionalProperties' in schema) && schema.patternProperties === undefined) {
schema.additionalProperties = true
schema.additionalProperties = false
}
})

169 changes: 158 additions & 11 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,10 @@ import {
TTuple,
T_UNKNOWN,
T_UNKNOWN_ADDITIONAL_PROPERTIES,
TIntersection
TIntersection,
TNamedInterfaceIntersection,
TInterfaceIntersection,
hasStandaloneName
} from './types/AST'
import {
getRootSchema,
@@ -23,7 +26,7 @@ import {
SchemaSchema,
SchemaType
} from './types/JSONSchema'
import {generateName, log, maybeStripDefault, maybeStripNameHints} from './utils'
import {INDEX_KEY_NAME, generateName, log, maybeStripDefault, maybeStripNameHints} from './utils'

export type Processed = Map<LinkedJSONSchema, Map<SchemaType, AST>>

@@ -123,14 +126,34 @@ function parseNonLiteral(
const keyNameFromDefinition = findKey(definitions, _ => _ === schema)

switch (type) {
case 'ALL_OF':
case 'ALL_OF': {
const extendsIndex = schema.allOf!.findIndex(_ => typeof _ !== 'boolean' && _.tsExtendAllOf)
if (extendsIndex >= 0) {
const name = standaloneName(schema, undefined, usedNames)!
const target = schema.allOf![extendsIndex] as SchemaSchema
return {
comment: schema.description,
keyName,
params: parseSchema(target, options, processed, usedNames, name),
standaloneName: name,
superTypes: schema
.allOf!.filter((_, i) => i !== extendsIndex)
.map(_ =>
ensureNamedInterface(parse(_ as SchemaSchema | boolean, options, undefined, processed, usedNames))
),
tsGenericParams: schema.tsGenericParams,
tsGenericValues: schema.tsGenericValues,
type: 'INTERFACE'
}
}
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
params: schema.allOf!.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'INTERSECTION'
}
}
case 'ANY':
return {
...(options.unknownAny ? T_UNKNOWN : T_ANY),
@@ -174,6 +197,13 @@ function parseNonLiteral(
}
case 'NAMED_SCHEMA':
return newInterface(schema as SchemaSchema, options, processed, usedNames, keyName)
case 'NEVER':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
type: 'NEVER'
}
case 'NULL':
return {
comment: schema.description,
@@ -204,6 +234,18 @@ function parseNonLiteral(
type: 'UNION'
}
case 'REFERENCE':
// if (schema.$ref === '#') {
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
// const {$ref, ...fixedSchema} = schema
// return parse(fixedSchema, options, keyName, processed, usedNames)
// }
// import $RefParser = require('@apidevtools/json-schema-ref-parser')
// const resolver = new $RefParser()
// const rootSchema = getRootSchema(schema)
// resolver.parse(rootSchema)
// const resolved = resolver.$refs.get(schema.$ref!)
// log('blue', 'parser', schema.$ref, 'resolved', resolved)
// return parse(resolved, options, rootSchema, keyName, true, processed, usedNames)
throw Error(format('Refs should have been resolved by the resolver!', schema))
case 'STRING':
return {
@@ -253,6 +295,29 @@ function parseNonLiteral(
type: 'UNION'
}
case 'UNNAMED_ENUM':
if (schema.tsEnumRef) {
const enumAst = parse(schema.tsEnumRef, options, undefined, processed, usedNames)
if (enumAst.type !== 'ENUM') {
throw Error(format('tsEnumRef does not resolve to an enum!', schema))
}
const params = schema.enum!.map<AST>(_value => {
const enumParam = enumAst.params.find(_ => _.ast.type === 'LITERAL' && _.ast.params === _value)
if (!enumParam) {
throw new Error(format('%j does not exist in referenced enum', _value, schema))
}
return {
params: [enumAst, enumParam],
type: 'TYPE_REFERENCE'
}
})
return {
comment: schema.description,
keyName,
params,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
type: 'UNION'
}
}
return {
comment: schema.description,
keyName,
@@ -306,21 +371,88 @@ function standaloneName(
}
}

/**
* New interface _OR_ named object definition
*/
function newInterface(
schema: SchemaSchema,
options: Options,
processed: Processed,
usedNames: UsedNames,
keyName?: string,
keyNameFromDefinition?: string
): TInterface {
): TInterface | TInterfaceIntersection {
const name = standaloneName(schema, keyNameFromDefinition, usedNames)!
const params = parseSchema(schema, options, processed, usedNames, name)

const propertyNamesSchema = schema.propertyNames
if (
propertyNamesSchema &&
propertyNamesSchema !== true &&
!((propertyNamesSchema.pattern || propertyNamesSchema.format) && !propertyNamesSchema.enum)
) {
if (schema.extends) {
// Don't use < draft4 features with a draft6+ feature.
throw Error(format('Supertype forbidden with propertyNames!', schema))
}
const paramsKeyType = parse(propertyNamesSchema, options, undefined, processed, usedNames)
const comment = `This interface was referenced by \`${name}\`'s JSON-Schema definition
via \`propertyNames\`.`
paramsKeyType.comment = paramsKeyType.comment ? `${paramsKeyType.comment}\n\n${comment}` : comment

if (!hasStandaloneName(paramsKeyType)) {
throw Error(format('Type for property names must have standalone name!', propertyNamesSchema, paramsKeyType))
}

const additionalPropIndex = params.findIndex(param => param.keyName === INDEX_KEY_NAME)
const additionalProp: TInterfaceParam =
additionalPropIndex >= 0
? params[additionalPropIndex]
: {
ast: {type: 'ANY'},
keyName: INDEX_KEY_NAME,
isPatternProperty: false,
isRequired: false,
isUnreachableDefinition: false
}
if (additionalPropIndex >= 0) {
params.splice(additionalPropIndex, 1)
}
const mappedType: TInterface = {
paramsKeyType,
params: [additionalProp],
superTypes: [],
type: 'INTERFACE'
}
const rootType = {
keyName,
standaloneName: name,
comment: schema.description,
tsGenericParams: schema.tsGenericParams,
tsGenericValues: schema.tsGenericValues
}
// TODO: handle "required".
if (params.length === 0) {
additionalProp.keyName = `[K in ${paramsKeyType.standaloneName}]`
return {...mappedType, ...rootType}
}
const knownKeys = params.map(param => JSON.stringify(param.keyName)).join(' | ')
additionalProp.keyName = `[K in Exclude<${paramsKeyType.standaloneName}, ${knownKeys}>]`
return {
...rootType,
params: [mappedType, {params, superTypes: [], type: 'INTERFACE'}],
type: 'INTERSECTION'
}
}

return {
comment: schema.description,
keyName,
params: parseSchema(schema, options, processed, usedNames, name),
params,
standaloneName: name,
superTypes: parseSuperTypes(schema, options, processed, usedNames),
tsGenericParams: schema.tsGenericParams,
tsGenericValues: schema.tsGenericValues,
type: 'INTERFACE'
}
}
@@ -330,7 +462,7 @@ function parseSuperTypes(
options: Options,
processed: Processed,
usedNames: UsedNames
): TNamedInterface[] {
): (TNamedInterface | TNamedInterfaceIntersection)[] {
// Type assertion needed because of dereferencing step
// TODO: Type it upstream
const superTypes = schema.extends as SchemaSchema[] | undefined
@@ -340,6 +472,21 @@ function parseSuperTypes(
return superTypes.map(_ => parse(_, options, undefined, processed, usedNames) as TNamedInterface)
}

function ensureNamedInterface(ast: AST): TNamedInterface | TNamedInterfaceIntersection {
if (!ast.standaloneName) {
throw Error(format('Supertype must have standalone name!', ast))
}
switch (ast.type) {
case 'INTERFACE':
return ast as TNamedInterface
case 'INTERSECTION':
if (ast.params.every(p => p.type === 'INTERFACE')) {
return ast as TNamedInterfaceIntersection
}
}
throw Error(format('Invalid supertype!', ast))
}

/**
* Helper to parse schema properties into params on the parent schema's type
*/
@@ -376,7 +523,7 @@ via the \`patternProperty\` "${key}".`
isPatternProperty: !singlePatternProperty,
isRequired: singlePatternProperty || includes(schema.required || [], key),
isUnreachableDefinition: false,
keyName: singlePatternProperty ? '[k: string]' : key
keyName: singlePatternProperty ? INDEX_KEY_NAME : key
}
})
)
@@ -402,7 +549,6 @@ via the \`definition\` "${key}".`

// handle additionalProperties
switch (schema.additionalProperties) {
case undefined:
case true:
if (singlePatternProperty) {
return asts
@@ -412,21 +558,22 @@ via the \`definition\` "${key}".`
isPatternProperty: false,
isRequired: true,
isUnreachableDefinition: false,
keyName: '[k: string]'
keyName: INDEX_KEY_NAME
})

case undefined:
case false:
return asts

// pass "true" as the last param because in TS, properties
// defined via index signatures are already optional
default:
return asts.concat({
ast: parse(schema.additionalProperties, options, '[k: string]', processed, usedNames),
ast: parse(schema.additionalProperties, options, INDEX_KEY_NAME, processed, usedNames),
isPatternProperty: false,
isRequired: true,
isUnreachableDefinition: false,
keyName: '[k: string]'
keyName: INDEX_KEY_NAME
})
}
}
7 changes: 4 additions & 3 deletions src/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import $RefParser = require('json-schema-ref-parser')
import {JSONSchema4} from 'json-schema'
import $RefParser = require('@apidevtools/json-schema-ref-parser')
import {JSONSchema} from './types/JSONSchema'
import {log} from './utils'

export async function dereference(
schema: JSONSchema,
{cwd, $refOptions}: {cwd: string; $refOptions: $RefParser.Options}
): Promise<JSONSchema> {
log('green', 'dereferencer', 'Dereferencing input schema:', cwd, schema)
log('green', 'dereferencer', 'Dereferencing input schema:', cwd, schema, $refOptions)
const parser = new $RefParser()
return parser.dereference(cwd, schema, $refOptions)
return parser.dereference(cwd, schema as JSONSchema4, $refOptions)
}
34 changes: 29 additions & 5 deletions src/types/AST.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ export type AST =
| TNamedInterface
| TIntersection
| TLiteral
| TNever
| TNumber
| TNull
| TObject
@@ -20,6 +21,7 @@ export type AST =
| TUnion
| TUnknown
| TCustomType
| TTypeReference

export interface AbstractAST {
comment?: string
@@ -66,17 +68,23 @@ export interface TEnumParam {
keyName: string
}

export interface TTypeReference extends AbstractAST {
type: 'TYPE_REFERENCE'
// Note: this can be expanded for other advanced type referencing, to reduce duplication
params: [TEnum, TEnumParam]
}

export interface TInterface extends AbstractAST {
type: 'INTERFACE'
params: TInterfaceParam[]
superTypes: TNamedInterface[]
superTypes: (TNamedInterface | TNamedInterfaceIntersection)[]
paramsKeyType?: ASTWithStandaloneName
tsGenericParams?: string[]
tsGenericValues?: { [name: string]: string[] }
}

export interface TNamedInterface extends AbstractAST {
export interface TNamedInterface extends TInterface {
standaloneName: string
type: 'INTERFACE'
params: TInterfaceParam[]
superTypes: TNamedInterface[]
}

export interface TInterfaceParam {
@@ -90,13 +98,27 @@ export interface TInterfaceParam {
export interface TIntersection extends AbstractAST {
type: 'INTERSECTION'
params: AST[]
tsGenericParams?: string[]
tsGenericValues?: { [name: string]: string[] }
}

export interface TInterfaceIntersection extends TIntersection {
params: TInterface[]
}

export interface TNamedInterfaceIntersection extends TInterfaceIntersection {
standaloneName: string
}

export interface TLiteral extends AbstractAST {
params: JSONSchema4Type
type: 'LITERAL'
}

export interface TNever extends AbstractAST {
type: 'NEVER'
}

export interface TNumber extends AbstractAST {
type: 'NUMBER'
}
@@ -129,6 +151,8 @@ export interface TTuple extends AbstractAST {
export interface TUnion extends AbstractAST {
type: 'UNION'
params: AST[]
tsGenericParams?: string[]
tsGenericValues?: { [name: string]: string[] }
}

export interface TUnknown extends AbstractAST {
62 changes: 59 additions & 3 deletions src/types/JSONSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {JSONSchema4, JSONSchema4Type, JSONSchema4TypeName} from 'json-schema'
import {JSONSchema4, JSONSchema4Type, JSONSchema4TypeName, JSONSchema6} from 'json-schema'
import {isPlainObject, memoize} from 'lodash'

export type SchemaType =
@@ -20,19 +20,75 @@ export type SchemaType =
| 'UNNAMED_ENUM'
| 'UNTYPED_ARRAY'
| 'CUSTOM_TYPE'
| 'NEVER'

export type JSONSchemaTypeName = JSONSchema4TypeName
export type JSONSchemaType = JSONSchema4Type

export interface JSONSchema extends JSONSchema4 {
export type JSONSchemaDefinition = boolean | JSONSchema
type IncompatibleKeys =
| 'exclusiveMinimum'
| 'exclusiveMaximum'
| 'additionalItems'
| 'items'
| 'required'
| 'additionalProperties'
| 'patternProperties'
| 'definitions'
| 'dependencies'
| 'properties'
| 'allOf'
| 'anyOf'
| 'oneOf'
| 'not'
type ReplaceSchemaTypeDeep<T> = T extends {[key: string]: infer U}
? {[key: string]: ReplaceSchemaTypeArr<U>}
: ReplaceSchemaTypeArr<T>
type ReplaceSchemaTypeArr<T> = [
ReplaceSchemaType<Extract<T, any[]>[number]>[],
ReplaceSchemaType<Exclude<T, any[]>>
][T extends any[] ? 0 : 1]
type ReplaceSchemaType<T> = T extends JSONSchema4 | JSONSchema6 ? JSONSchema : T
export interface JSONSchema extends Omit<JSONSchema4, IncompatibleKeys>, Omit<JSONSchema6, IncompatibleKeys> {
additionalItems?: ReplaceSchemaType<JSONSchema4['additionalItems'] | JSONSchema6['additionalItems']>
additionalProperties?: ReplaceSchemaType<JSONSchema4['additionalProperties'] | JSONSchema6['additionalProperties']>
patternProperties?: ReplaceSchemaTypeDeep<JSONSchema4['patternProperties'] | JSONSchema6['patternProperties']>
properties?: ReplaceSchemaTypeDeep<JSONSchema4['properties'] | JSONSchema6['properties']>
items?: ReplaceSchemaTypeArr<JSONSchema4['items'] | JSONSchema6['items']>
allOf?: ReplaceSchemaTypeArr<JSONSchema4['allOf'] | JSONSchema6['allOf']>
anyOf?: ReplaceSchemaTypeArr<JSONSchema4['anyOf'] | JSONSchema6['anyOf']>
oneOf?: ReplaceSchemaTypeArr<JSONSchema4['oneOf'] | JSONSchema6['oneOf']>
required?: JSONSchema4['required'] | JSONSchema6['required']
definitions?: ReplaceSchemaTypeDeep<JSONSchema4['definitions'] | JSONSchema6['definitions']>
dependencies?: ReplaceSchemaTypeDeep<JSONSchema4['dependencies'] | JSONSchema6['dependencies']>
exclusiveMinimum?: JSONSchema4['exclusiveMinimum'] | JSONSchema6['exclusiveMinimum']
exclusiveMaximum?: JSONSchema4['exclusiveMaximum'] | JSONSchema6['exclusiveMaximum']
not?: ReplaceSchemaType<JSONSchema4['not'] | JSONSchema6['not']>
/**
* schema extension to support numeric enums
* schema extension to support defined enums
*/
tsEnumNames?: string[]
/**
* schema extension to support using an enum
*/
tsEnumRef?: JSONSchema
/**
* schema extension to support custom types
*/
tsType?: string
/**
* schema extension to support generic parameter names
*/
tsGenericParams?: string[]
/**
* schema extension to support generic parameter values
*/
tsGenericValues?: {[key: string]: string[]}
/**
* schema extension to use inside of an "allOf" to note that this type should
* "extends" the other types in the allOf, instead of using intersection.
*/
tsExtendAllOf?: boolean
}

export const Parent = Symbol('Parent')
3 changes: 3 additions & 0 deletions src/typesOfSchema.ts
Original file line number Diff line number Diff line change
@@ -67,6 +67,9 @@ const matchers: Record<SchemaType, (schema: JSONSchema) => boolean> = {
NAMED_SCHEMA(schema) {
return 'id' in schema && ('patternProperties' in schema || 'properties' in schema)
},
NEVER() {
return false
},
NULL(schema) {
return schema.type === 'null'
},
28 changes: 26 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ import {deburr, isPlainObject, mapValues, trim, upperFirst} from 'lodash'
import {basename, dirname, extname, join, normalize, sep} from 'path'
import {JSONSchema, LinkedJSONSchema} from './types/JSONSchema'

export const INDEX_KEY_NAME = '[k: string]'

// TODO: pull out into a separate package
// eslint-disable-next-line
export function Try<T>(fn: () => T, err: (e: Error) => any): T {
@@ -74,8 +76,9 @@ function traverseObjectKeys(
processed: Set<LinkedJSONSchema>
) {
Object.keys(obj).forEach(k => {
if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) {
traverse(obj[k], callback, processed)
const item = obj[k]
if (item && typeof item === 'object' && !Array.isArray(item)) {
traverse(item, callback, processed)
}
})
}
@@ -294,6 +297,27 @@ export function pathTransform(outputPath: string, inputPath: string, filePath: s
return join(normalize(outputPath), ...filePathRel)
}

export function extractStrings(schema: JSONSchema): string[] | null {
if (schema.enum) {
return schema.enum.filter((value): value is string => typeof value === 'string')
}
const unionItems = schema.oneOf || schema.anyOf
if (!unionItems) {
return null
}
let results: string[] = []
for (const item of unionItems) {
if (typeof item !== 'boolean') {
const subresult = extractStrings(item)
if (!subresult) {
return null
}
results = results.concat(subresult)
}
}
return results
}

/**
* Removes the schema's `default` property if it doesn't match the schema's `type` property.
* Useful when parsing unions.
695 changes: 183 additions & 512 deletions test/__snapshots__/test/test.ts.md

Large diffs are not rendered by default.

Binary file modified test/__snapshots__/test/test.ts.snap
Binary file not shown.
18 changes: 18 additions & 0 deletions test/e2e/enumReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const input = {
"title": "Enum",
"type": "object",
"properties": {
"stringEnum": {
"type": "string",
"enum": ["a", "b", "c"],
"tsEnumNames": ['A', 'B', 'C']
},
"stringEnumItem": {
"type": "string",
"enum": ["a"],
"tsEnumRef": { "$ref": "#/properties/stringEnum" }
}
},
required: ['stringEnum', 'stringEnumItem'],
additionalProperties: false
}
4 changes: 2 additions & 2 deletions test/normalizer/defaultAdditionalProperties.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "Default additionalProperties to true",
"name": "Default additionalProperties to false",
"in": {
"id": "foo",
"type": ["object"],
@@ -21,6 +21,6 @@
}
},
"required": [],
"additionalProperties": true
"additionalProperties": false
}
}
2 changes: 1 addition & 1 deletion test/normalizer/redundantNull.json
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@
}
},
"out": {
"additionalProperties": true,
"additionalProperties": false,
"id": "RedundantNull",
"required": [],
"type": "object",
5 changes: 3 additions & 2 deletions test/testE2E.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import test from 'ava'
import {readdirSync} from 'fs'
import {JSONSchema4, JSONSchema6} from 'json-schema'
import {find} from 'lodash'
import {join} from 'path'
import {compile, JSONSchema, Options} from '../src'
import {compile, Options} from '../src'
import {log, stripExtension} from '../src/utils'

const dir = __dirname + '/e2e'

type TestCase = {
input: JSONSchema
input: JSONSchema4 | JSONSchema6
error?: true
exclude?: boolean
only?: boolean
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
"target": "es5"
},
"exclude": [
"dist",
"example",
"node_modules"
]
2 changes: 1 addition & 1 deletion types/json-schema-ref-parser.d.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.1

declare module 'json-schema-ref-parser' {
declare module '@apidevtools/json-schema-ref-parser' {

import { JSONSchema4, JSONSchema4Type } from 'json-schema'

19 changes: 6 additions & 13 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -2,12 +2,12 @@
# yarn lockfile v1


"@apidevtools/json-schema-ref-parser@9.0.6":
version "9.0.6"
resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c"
integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==
"@apidevtools/json-schema-ref-parser@^9.0.1":
version "9.0.1"
resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.1.tgz#c0ed0bd21a7397d2d7a83b69565268f2d78f2d7a"
integrity sha512-Qsdz0W0dyK84BuBh5KZATWXOtVDXIF2EeNRzpyWblPUeAmnIokwWcwrpAm5pTPMjuWoIQt9C67X3Af1OlL6oSw==
dependencies:
"@jsdevtools/ono" "^7.1.3"
"@jsdevtools/ono" "^7.1.2"
call-me-maybe "^1.0.1"
js-yaml "^3.13.1"

@@ -55,7 +55,7 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"

"@jsdevtools/ono@^7.1.3":
"@jsdevtools/ono@^7.1.2", "@jsdevtools/ono@^7.1.3":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
@@ -2337,13 +2337,6 @@ json-parse-even-better-errors@^2.3.0:
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==

json-schema-ref-parser@^9.0.6:
version "9.0.6"
resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#fc89a5e6b853f2abe8c0af30d3874196526adb60"
integrity sha512-z0JGv7rRD3CnJbZY/qCpscyArdtLJhr/wRBmFUdoZ8xMjsFyNdILSprG2degqRLjBjyhZHAEBpGOxniO9rKTxA==
dependencies:
"@apidevtools/json-schema-ref-parser" "9.0.6"

json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"