json-schema-to-typescript
Version:
compile json schema to typescript typings
150 lines (144 loc) • 3.92 kB
text/typescript
import {isPlainObject} from 'lodash'
import {isCompound, JSONSchema, SchemaType} from './types/JSONSchema'
/**
* Duck types a JSONSchema schema or property to determine which kind of AST node to parse it into.
*
* Due to what some might say is an oversight in the JSON-Schema spec, a given schema may
* implicitly be an *intersection* of multiple JSON-Schema directives (ie. multiple TypeScript
* types). The spec leaves it up to implementations to decide what to do with this
* loosely-defined behavior.
*/
export function typesOfSchema(schema: JSONSchema): Set<SchemaType> {
// tsType is an escape hatch that supercedes all other directives
if (schema.tsType) {
return new Set(['CUSTOM_TYPE'])
}
// Collect matched types
const matchedTypes = new Set<SchemaType>()
for (const [schemaType, f] of Object.entries(matchers)) {
if (f(schema)) {
matchedTypes.add(schemaType as SchemaType)
}
}
// Default to an unnamed schema
if (!matchedTypes.size) {
matchedTypes.add('UNNAMED_SCHEMA')
}
return matchedTypes
}
const matchers: Record<SchemaType, (schema: JSONSchema) => boolean> = {
ALL_OF(schema) {
return 'allOf' in schema
},
ANY(schema) {
if (Object.keys(schema).length === 0) {
// The empty schema {} validates any value
// @see https://json-schema.org/draft-07/json-schema-core.html#rfc.section.4.3.1
return true
}
return schema.type === 'any'
},
ANY_OF(schema) {
return 'anyOf' in schema
},
BOOLEAN(schema) {
if ('enum' in schema) {
return false
}
if (schema.type === 'boolean') {
return true
}
if (!isCompound(schema) && typeof schema.default === 'boolean') {
return true
}
return false
},
CUSTOM_TYPE() {
return false // Explicitly handled before we try to match
},
NAMED_ENUM(schema) {
return 'enum' in schema && 'tsEnumNames' in schema
},
NAMED_SCHEMA(schema) {
// 8.2.1. The presence of "$id" in a subschema indicates that the subschema constitutes a distinct schema resource within a single schema document.
return '$id' in schema && ('patternProperties' in schema || 'properties' in schema)
},
NEVER(schema: JSONSchema | boolean) {
return schema === false
},
NULL(schema) {
return schema.type === 'null'
},
NUMBER(schema) {
if ('enum' in schema) {
return false
}
if (schema.type === 'integer' || schema.type === 'number') {
return true
}
if (!isCompound(schema) && typeof schema.default === 'number') {
return true
}
return false
},
OBJECT(schema) {
return (
schema.type === 'object' &&
!isPlainObject(schema.additionalProperties) &&
!schema.allOf &&
!schema.anyOf &&
!schema.oneOf &&
!schema.patternProperties &&
!schema.properties &&
!schema.required
)
},
ONE_OF(schema) {
return 'oneOf' in schema
},
REFERENCE(schema) {
return '$ref' in schema
},
STRING(schema) {
if ('enum' in schema) {
return false
}
if (schema.type === 'string') {
return true
}
if (!isCompound(schema) && typeof schema.default === 'string') {
return true
}
return false
},
TYPED_ARRAY(schema) {
if (schema.type && schema.type !== 'array') {
return false
}
return 'items' in schema
},
UNION(schema) {
return Array.isArray(schema.type)
},
UNNAMED_ENUM(schema) {
if ('tsEnumNames' in schema) {
return false
}
if (
schema.type &&
schema.type !== 'boolean' &&
schema.type !== 'integer' &&
schema.type !== 'number' &&
schema.type !== 'string'
) {
return false
}
return 'enum' in schema
},
UNNAMED_SCHEMA() {
return false // Explicitly handled as the default case
},
UNTYPED_ARRAY(schema) {
return schema.type === 'array' && !('items' in schema)
},
}