Skip to content

Commit e356fcd

Browse files
committed
feat: add exported toJsonSchema function
1 parent 220da1e commit e356fcd

File tree

6 files changed

+489
-2
lines changed

6 files changed

+489
-2
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ There are also reasons not to choose this package. Because of all it does, this
9393
- [Validate one key against another](#validate-one-key-against-another)
9494
- [Debug Mode](#debug-mode)
9595
- [Extending the Schema Options](#extending-the-schema-options)
96+
- [Converting a SimpleSchema to a JSONSchema](#converting-a-simpleschema-to-a-jsonschema)
9697
- [Add On Packages](#add-on-packages)
9798
- [Contributors](#contributors)
9899
- [Sponsors](#sponsors)
@@ -1243,6 +1244,18 @@ SimpleSchema.extendOptions(["index", "unique", "denyInsert", "denyUpdate"]);
12431244

12441245
Obviously you need to ensure that `extendOptions` is called before any SimpleSchema instances are created with those options.
12451246

1247+
## Converting a SimpleSchema to a JSONSchema
1248+
1249+
```ts
1250+
import { toJsonSchema } from 'simpl-schema'
1251+
1252+
const schema = new SimpleSchema({
1253+
name: String
1254+
})
1255+
1256+
const jsonSchema = toJsonSchema(schema)
1257+
```
1258+
12461259
## Add On Packages
12471260

12481261
[mxab:simple-schema-jsdoc](https://atmospherejs.com/mxab/simple-schema-jsdoc) Generate jsdoc from your schemas.

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
},
4444
"devDependencies": {
4545
"@types/clone": "^2.1.1",
46+
"@types/json-schema": "^7.0.11",
4647
"@types/mocha": "^9.1.1",
4748
"@typescript-eslint/eslint-plugin": "^5.30.7",
4849
"@typescript-eslint/parser": "^5.30.7",
@@ -85,4 +86,4 @@
8586
"publishConfig": {
8687
"access": "public"
8788
}
88-
}
89+
}

src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import './clean.js'
22

33
import { SimpleSchema, ValidationContext } from './SimpleSchema.js'
4+
import { toJsonSchema } from './toJsonSchema.js'
45

56
SimpleSchema.ValidationContext = ValidationContext
67

7-
export { ValidationContext }
8+
export { toJsonSchema, ValidationContext }
89

910
export default SimpleSchema

src/toJsonSchema.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import type { JSONSchema7 } from 'json-schema'
2+
3+
import { SimpleSchema } from './SimpleSchema.js'
4+
import { StandardSchemaKeyDefinition } from './types.js'
5+
6+
const jsonSchemaVersion = 'https://json-schema.org/draft/2020-12/schema'
7+
8+
function toJSArray (ss: SimpleSchema, key: string, fieldDef: StandardSchemaKeyDefinition): JSONSchema7 | null {
9+
const itemSchema = fieldDefToJsonSchema(ss, `${key}.$`)
10+
if (itemSchema == null) return null
11+
12+
const arrayDef: JSONSchema7 = {
13+
type: 'array',
14+
items: [itemSchema],
15+
additionalItems: false
16+
}
17+
18+
if (fieldDef.minCount !== undefined) {
19+
arrayDef.minItems = fieldDef.minCount
20+
}
21+
22+
if (fieldDef.maxCount !== undefined) {
23+
arrayDef.maxItems = fieldDef.maxCount
24+
}
25+
26+
return arrayDef
27+
}
28+
29+
function toJsProperties (ss: SimpleSchema): {
30+
properties: Record<string, JSONSchema7>
31+
required: string[]
32+
} {
33+
const properties: Record<string, JSONSchema7> = {}
34+
const required: string[] = []
35+
36+
for (const key of ss.objectKeys()) {
37+
const fieldDef = ss.schema(key)
38+
if (fieldDef == null) continue
39+
if (fieldDef.optional !== true) required.push(key)
40+
const schema = fieldDefToJsonSchema(ss, key)
41+
if (schema != null) properties[key] = schema
42+
}
43+
44+
return { properties, required }
45+
}
46+
47+
function toJSObj (simpleSchema: SimpleSchema, additionalProperties: boolean = false): JSONSchema7 | null {
48+
return {
49+
type: 'object',
50+
...toJsProperties(simpleSchema),
51+
additionalProperties
52+
}
53+
}
54+
55+
function fieldDefToJsonSchema (ss: SimpleSchema, key: string): JSONSchema7 | null {
56+
const fieldDef = ss.schema(key)
57+
if (fieldDef == null) return null
58+
59+
const itemSchemas = []
60+
61+
for (const fieldTypeDef of fieldDef.type.definitions) {
62+
let itemSchema: JSONSchema7 | null = null
63+
64+
switch (fieldTypeDef.type) {
65+
case String:
66+
itemSchema = { type: 'string' }
67+
if (fieldTypeDef.allowedValues !== undefined && typeof fieldTypeDef.allowedValues !== 'function') {
68+
itemSchema.enum = [...fieldTypeDef.allowedValues]
69+
}
70+
if (fieldTypeDef.max !== undefined && typeof fieldTypeDef.max !== 'function') {
71+
itemSchema.maxLength = fieldTypeDef.max as number
72+
}
73+
if (fieldTypeDef.min !== undefined && typeof fieldTypeDef.min !== 'function') {
74+
itemSchema.minLength = fieldTypeDef.min as number
75+
}
76+
if (fieldTypeDef.regEx instanceof RegExp) {
77+
itemSchema.pattern = String(fieldTypeDef.regEx)
78+
}
79+
break
80+
81+
case Number:
82+
case SimpleSchema.Integer:
83+
itemSchema = { type: fieldTypeDef.type === Number ? 'number' : 'integer' }
84+
if (fieldTypeDef.max !== undefined && typeof fieldTypeDef.max !== 'function') {
85+
if (fieldTypeDef.exclusiveMax === true) {
86+
itemSchema.exclusiveMaximum = fieldTypeDef.max as number
87+
} else {
88+
itemSchema.maximum = fieldTypeDef.max as number
89+
}
90+
}
91+
if (fieldTypeDef.min !== undefined && typeof fieldTypeDef.min !== 'function') {
92+
if (fieldTypeDef.exclusiveMin === true) {
93+
itemSchema.exclusiveMinimum = fieldTypeDef.min as number
94+
} else {
95+
itemSchema.minimum = fieldTypeDef.min as number
96+
}
97+
}
98+
break
99+
100+
case Boolean:
101+
itemSchema = { type: 'boolean' }
102+
break
103+
104+
case Date:
105+
itemSchema = {
106+
type: 'string',
107+
format: 'date-time'
108+
}
109+
break
110+
111+
case Array:
112+
itemSchema = toJSArray(ss, key, fieldDef)
113+
break
114+
115+
case Object:
116+
itemSchema = toJSObj(ss.getObjectSchema(key), fieldTypeDef.blackbox)
117+
break
118+
119+
case SimpleSchema.Any:
120+
// In JSONSchema an empty object means any type
121+
itemSchema = {}
122+
break
123+
124+
default:
125+
if (SimpleSchema.isSimpleSchema(fieldTypeDef.type)) {
126+
itemSchema = toJSObj(fieldTypeDef.type as SimpleSchema, fieldTypeDef.blackbox)
127+
} else if (
128+
// support custom objects
129+
fieldTypeDef.type instanceof Function
130+
) {
131+
itemSchema = toJSObj(ss.getObjectSchema(key), fieldTypeDef.blackbox)
132+
}
133+
break
134+
}
135+
136+
if (itemSchema != null && fieldTypeDef.defaultValue !== undefined) {
137+
itemSchema.default = fieldTypeDef.defaultValue
138+
}
139+
140+
if (itemSchema != null) itemSchemas.push(itemSchema)
141+
}
142+
143+
if (itemSchemas.length > 1) {
144+
return { anyOf: itemSchemas }
145+
}
146+
147+
return itemSchemas[0] ?? null
148+
}
149+
150+
/**
151+
* Convert a SimpleSchema to a JSONSchema Document.
152+
*
153+
* Notes:
154+
* - Date fields will become string fields with built-in 'date-time' format.
155+
* - JSONSchema does not support minimum or maximum values for date fields
156+
* - Custom validators are ignored
157+
* - Field definition properties that are a function are ignored
158+
* - Custom objects are treated as regular objects
159+
*
160+
* @param simpleSchema SimpleSchema instance to convert
161+
* @param id Optional ID to use for the `$id` field
162+
* @returns JSONSchema Document
163+
*/
164+
export function toJsonSchema (simpleSchema: SimpleSchema, id?: string): JSONSchema7 {
165+
return {
166+
...(id != null ? { $id: id } : {}),
167+
$schema: jsonSchemaVersion,
168+
...toJSObj(simpleSchema)
169+
}
170+
}

0 commit comments

Comments
 (0)