joi-to-json 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,125 +1,272 @@
1
- # joi-to-json
2
-
3
- ## Objective
4
-
5
- I have been using [joi](https://joi.dev/) a lot in different Node.js projects to guard the API.
6
- It's **The most powerful schema description language and data validator for JavaScript.** as it said.
7
-
8
- Many times, we need to utilize this schema description to produce other output, such as Swagger OpenAPI doc.
9
- That is why I build [joi-route-to-swagger](https://github.com/kenspirit/joi-route-to-swagger) in the first place.
10
-
11
- At the beginning, `joi-route-to-swagger` relies on [joi-to-json-schema](https://github.com/lightsofapollo/joi-to-json-schema/) which utilizes many joi internal api or properties. I believed there was reason. Maybe joi did not provide the `describe` api way before. But I always feel uncomfortable and think it's time to move on.
12
-
13
- The intention of `joi-to-json` is to support converting different version's joi schema to [JSON Schema](https://json-schema.org) using `describe` api.
14
-
15
- ## 2.0.0 is out
16
-
17
- It's a breaking change.
18
-
19
- * Functionally, output format supports OpenAPI Schema other than purely JSON Schema.
20
-
21
- * Technically, implementation theory has a big change:
22
- - In v1.0.0, it directly converts `joi.describe()` to JSON schema using different parser implementations.
23
- - In v2.0.0, `joi.describe()` of different versions are first converted to one base format, the latest version of `joi.describe()` output. Then different parsers (JSON, OpenAPI) all refer to this base format.
24
-
25
- * The benefits of the change are:
26
- - Easier to retire old version of joi.
27
- - Easier to support more output formats.
28
-
29
- ## Installation
30
-
31
- >npm install joi-to-json
32
-
33
-
34
- ## Joi Version Support
35
-
36
- * @commercial/joi
37
- * v12.1.0
38
- * joi
39
- * 13.7.0
40
- * 14.3.1
41
- * @hapi/joi
42
- * 15.1.1
43
- * 16.1.8
44
- * joi
45
- * 17.4.2
46
-
47
- For all above versions, I have tested one complex joi object [fixtures](./fixtures) which covers most of the JSON schema attributes that can be described in joi schema.
48
-
49
- Although the versions chosen are the latest one for each major version, I believe it should be supporting other minor version as well.
50
-
51
-
52
- ## Usage
53
-
54
- Only one API `parse` is available. It's signature is `parse(joiObj, type = 'json')`
55
-
56
- Currently supported output types:
57
- * `json` - Default. Stands for JSON Schema Draft 07
58
- * `open-api` - Stands for OpenAPI Schema
59
- * `json-draft-04` - Stands for JSON Schema Draft 04
60
- * `json-draft-2019-09` - Stands for JSON Schema Draft 2019-09
61
-
62
- The output schema format are in [outputs](./outputs) under specific folders for different types.
63
-
64
- Sample code is as below:
65
-
66
- ```javascript
67
- const parse = require('joi-to-json')
68
-
69
- const joiSchema = joi.object().keys({
70
- nickName: joi.string().required().min(3).max(20).example('鹄思乱想').description('Hero Nickname')
71
- .regex(/^[a-z]+$/, { name: 'alpha', invert: true }),
72
- avatar: joi.string().required().uri(),
73
- email: joi.string().email(),
74
- ip: joi.string().ip({ version: ['ipv4', 'ipv6'] }),
75
- hostname: joi.string().hostname().insensitive(),
76
- gender: joi.string().valid('Male', 'Female', '').default('Male'),
77
- height: joi.number().precision(2).positive().greater(0).less(200),
78
- birthday: joi.date().iso(),
79
- birthTime: joi.date().timestamp('unix'),
80
- skills: joi.array().items(joi.alternatives().try(
81
- joi.string(),
82
- joi.object().keys({
83
- name: joi.string().example('teleport').alphanum().lowercase().required().description('Skill Name'),
84
- level: joi.number().integer().min(10).max(100).default(50).multiple(10).example(10).description('Skill Level')
85
- })
86
- ).required()).min(1).max(3).unique().description('Skills'),
87
- tags: joi.array().items(joi.string().required()).length(2),
88
- retired: joi.boolean().truthy('yes').falsy('no').insensitive(false),
89
- certificate: joi.binary().encoding('base64'),
90
- notes: joi.any().meta({ 'x-supported-lang': ['zh-CN', 'en-US'], deprecated: true })
91
- })
92
-
93
- const jsonSchema = parse(joiSchema)
94
- // Or parsing to OpenAPI schema through:
95
- // const openApiSchema = parse(joiSchema, 'open-api')
96
- ```
97
-
98
- ## Browser support
99
- For generating JSON Schema in a browser you should use below import syntax for `joi` library in order to work because the `joi` browser minimized build does not have `describe` api which the `joi-to-json` relies on.
100
-
101
- ```typescript
102
- import Joi from 'joi/lib/index';
103
- ```
104
-
105
- ## Test
106
-
107
- >npm run test
108
-
109
- You can optionally set below environment variables:
110
-
111
- * `CASE_PATTERN=joi-obj-17` to control which version of joi obj to test
112
-
113
- ## Known Limitation
114
-
115
- * For `object.pattern` usage in Joi, `pattern` parameter can only be a regular expression now as I cannot convert Joi object to regex yet.
116
-
117
- ## Updates
118
-
119
- **Version 2.3.0**
120
-
121
- * Supports named link for schema resuse, such as `.link('#person')`. **For `open-api` conversion**, as the shared schemas are located in `#/components/schemas` which is not self-contained, the conversion result contains an **extra `schemas`** field so that you can extract it when required.
122
-
123
- ## License
124
-
125
- MIT
1
+ # joi-to-json
2
+
3
+ ## Objective
4
+
5
+ I have been using [joi](https://joi.dev/) a lot in different Node.js projects to guard the API.
6
+ It's **The most powerful schema description language and data validator for JavaScript.** as it said.
7
+
8
+ Many times, we need to utilize this schema description to produce other output, such as Swagger OpenAPI doc.
9
+ That is why I build [joi-route-to-swagger](https://github.com/kenspirit/joi-route-to-swagger) in the first place.
10
+
11
+ At the beginning, `joi-route-to-swagger` relies on [joi-to-json-schema](https://github.com/lightsofapollo/joi-to-json-schema/) which utilizes many joi internal api or properties. I believed there was reason. Maybe joi did not provide the `describe` api way before. But I always feel uncomfortable and think it's time to move on.
12
+
13
+ The intention of `joi-to-json` is to support converting different version's joi schema to [JSON Schema](https://json-schema.org) using `describe` api.
14
+
15
+ ## 2.0.0 is out
16
+
17
+ It's a breaking change.
18
+
19
+ * Functionally, output format supports OpenAPI Schema other than purely JSON Schema.
20
+
21
+ * Technically, implementation theory has a big change:
22
+ - In v1.0.0, it directly converts `joi.describe()` to JSON schema using different parser implementations.
23
+ - In v2.0.0, `joi.describe()` of different versions are first converted to one base format, the latest version of `joi.describe()` output. Then different parsers (JSON, OpenAPI) all refer to this base format.
24
+
25
+ * The benefits of the change are:
26
+ - Easier to retire old version of joi.
27
+ - Easier to support more output formats.
28
+
29
+ ## Installation
30
+
31
+ >npm install joi-to-json
32
+
33
+
34
+ ## Joi Version Support
35
+
36
+ * @commercial/joi
37
+ * v12.1.0
38
+ * joi
39
+ * 13.7.0
40
+ * 14.3.1
41
+ * @hapi/joi
42
+ * 15.1.1
43
+ * 16.1.8
44
+ * joi
45
+ * 17.4.2
46
+
47
+ For all above versions, I have tested one complex joi object [fixtures](./fixtures) which covers most of the JSON schema attributes that can be described in joi schema.
48
+
49
+ Although the versions chosen are the latest one for each major version, I believe it should be supporting other minor version as well.
50
+
51
+
52
+ ## Usage
53
+
54
+ Only one API `parse` is available. It's signature is `parse(joiObj, type = 'json')`
55
+
56
+ Currently supported output types:
57
+ * `json` - Default. Stands for JSON Schema Draft 07
58
+ * `open-api` - Stands for OpenAPI Schema
59
+ * `json-draft-04` - Stands for JSON Schema Draft 04
60
+ * `json-draft-2019-09` - Stands for JSON Schema Draft 2019-09
61
+
62
+ The output schema format are in [outputs](./outputs) under specific folders for different types.
63
+
64
+ Sample code is as below:
65
+
66
+ ```javascript
67
+ const parse = require('joi-to-json')
68
+
69
+ const joiSchema = joi.object().keys({
70
+ nickName: joi.string().required().min(3).max(20).example('鹄思乱想').description('Hero Nickname')
71
+ .regex(/^[a-z]+$/, { name: 'alpha', invert: true }),
72
+ avatar: joi.string().required().uri(),
73
+ email: joi.string().email(),
74
+ ip: joi.string().ip({ version: ['ipv4', 'ipv6'] }),
75
+ hostname: joi.string().hostname().insensitive(),
76
+ gender: joi.string().valid('Male', 'Female', '').default('Male'),
77
+ height: joi.number().precision(2).positive().greater(0).less(200),
78
+ birthday: joi.date().iso(),
79
+ birthTime: joi.date().timestamp('unix'),
80
+ skills: joi.array().items(joi.alternatives().try(
81
+ joi.string(),
82
+ joi.object().keys({
83
+ name: joi.string().example('teleport').alphanum().lowercase().required().description('Skill Name'),
84
+ level: joi.number().integer().min(10).max(100).default(50).multiple(10).example(10).description('Skill Level')
85
+ })
86
+ ).required()).min(1).max(3).unique().description('Skills'),
87
+ tags: joi.array().items(joi.string().required()).length(2),
88
+ retired: joi.boolean().truthy('yes').falsy('no').insensitive(false),
89
+ certificate: joi.binary().encoding('base64'),
90
+ notes: joi.any().meta({ 'x-supported-lang': ['zh-CN', 'en-US'], deprecated: true })
91
+ })
92
+
93
+ const jsonSchema = parse(joiSchema)
94
+ // Or parsing to OpenAPI schema through:
95
+ // const openApiSchema = parse(joiSchema, 'open-api')
96
+ ```
97
+
98
+ ### Joi to OpenAPI
99
+
100
+ Most Joi specifications result in the expected OpenAPI schema.
101
+
102
+ E.g.,
103
+
104
+ ```js
105
+ const joi = require('joi')
106
+ const { dump } = require('js-yaml')
107
+ const { writeFile } = require('fs/promises')
108
+
109
+ const joiSchema = joi.object().keys({
110
+ uuid: joi.string().uuid({ version: ['uuidv3', 'uuidv5'] }),
111
+ nickName: joi.string().required().example('鹄思乱想').description('Hero Nickname').min(3).max(20).pattern(/^[a-z]+$/, { name: 'alpha', invert: true }),
112
+ avatar: joi.string().required().uri(),
113
+ email: joi.string().email(),
114
+ ip: joi.string().ip({ version: ['ipv4', 'ipv6'] }),
115
+ hostname: joi.string().hostname().insensitive(),
116
+ gender: joi.string().valid('Male', 'Female', '', null).default('Male'),
117
+ isoDateString: joi.string().isoDate(),
118
+ isoDurationString: joi.string().isoDuration(),
119
+ birthday: joi.date().iso(),
120
+ certificate: joi.binary().encoding('base64'),
121
+ tags: joi.array().items(joi.string().required()).length(2),
122
+ nested: joi.object().keys({
123
+ key: joi.string()
124
+ }).unknown(true)
125
+ }).unknown(false)
126
+
127
+ async function writeYAML(targetPath) {
128
+ const openApiSchema = parse(joiSchema, 'open-api')
129
+
130
+ const openApiSchemaYAML = dump(openApiSchema, {lineWidth: 120, noCompatMode: true})
131
+ await writeFile(targetPath, openApiSchemaYAML)
132
+ }
133
+ ```
134
+
135
+ results in
136
+
137
+ ```yaml
138
+ type: object
139
+ required:
140
+ - nickName
141
+ - avatar
142
+ properties:
143
+ uuid:
144
+ type: string
145
+ format: uuid
146
+ nickName:
147
+ description: Hero Nickname
148
+ type: string
149
+ pattern: ^[a-z]+$
150
+ minLength: 3,
151
+ maxLength: 20,
152
+ example: 鹄思乱想
153
+ avatar:
154
+ type: string
155
+ format: uri
156
+ email:
157
+ type: string
158
+ format: email
159
+ ip:
160
+ type: string
161
+ oneOf:
162
+ - format: ipv4
163
+ - format: ipv6
164
+ hostname:
165
+ type: string
166
+ format: hostname
167
+ gender:
168
+ type: string
169
+ default: Male
170
+ enum:
171
+ - Male
172
+ - Female
173
+ - ''
174
+ - null
175
+ nullable: true
176
+ isoDateString:
177
+ type: string
178
+ format: date-time
179
+ isoDurationString:
180
+ type: string
181
+ format: duration
182
+ birthday:
183
+ type: string
184
+ format: date-time
185
+ certificate:
186
+ type: string
187
+ format: binary
188
+ tags:
189
+ type: array
190
+ items:
191
+ type: string
192
+ minItems: 2
193
+ maxItems: 2
194
+ nested:
195
+ type: object
196
+ properties:
197
+ key:
198
+ type: string
199
+ additionalProperties: true
200
+ additionalProperties: false
201
+ ```
202
+
203
+ Some OpenAPI features are not supported directly in Joi, but Joi schemas can be annotated with `joi.any().meta({…})`
204
+ to get them in the OpenAPI schema:
205
+
206
+ ```js
207
+
208
+
209
+ const joiSchema = joi.object().keys({
210
+ deprecatedProperty: joi.string().meta({ deprecated: true }).required(),
211
+ readOnlyProperty: joi.string().meta({ readOnly: true }),
212
+ writeOnlyProperty: joi.string().meta({ writeOnly: true }),
213
+ xMeta: joi.string().meta({ 'x-meta': 42 }),
214
+ unknownMetaProperty: joi.string().meta({ unknownMeta: 42 })
215
+ }).unknown(true)
216
+
217
+
218
+ ```
219
+
220
+ begets:
221
+
222
+ ```yaml
223
+ type: object
224
+ required:
225
+ - deprecatedProperty
226
+ properties:
227
+ deprecatedProperty:
228
+ type: string
229
+ deprecated: true
230
+ readOnlyProperty:
231
+ type: string
232
+ readOnly: true
233
+ writeOnlyProperty:
234
+ type: string
235
+ writeOnly: true
236
+ xMeta:
237
+ type: string
238
+ x-meta: 42
239
+ unknownMetaProperty:
240
+ type: string
241
+ # unknownMeta is not exported
242
+ additionalProperties: true
243
+ ```
244
+
245
+ ## Browser support
246
+ For generating JSON Schema in a browser you should use below import syntax for `joi` library in order to work because the `joi` browser minimized build does not have `describe` api which the `joi-to-json` relies on.
247
+
248
+ ```typescript
249
+ import Joi from 'joi/lib/index';
250
+ ```
251
+
252
+ ## Test
253
+
254
+ >npm run test
255
+
256
+ You can optionally set below environment variables:
257
+
258
+ * `CASE_PATTERN=joi-obj-17` to control which version of joi obj to test
259
+
260
+ ## Known Limitation
261
+
262
+ * For `object.pattern` usage in Joi, `pattern` parameter can only be a regular expression now as I cannot convert Joi object to regex yet.
263
+
264
+ ## Updates
265
+
266
+ **Version 2.3.0**
267
+
268
+ * Supports named link for schema resuse, such as `.link('#person')`. **For `open-api` conversion**, as the shared schemas are located in `#/components/schemas` which is not self-contained, the conversion result contains an **extra `schemas`** field so that you can extract it when required.
269
+
270
+ ## License
271
+
272
+ MIT
package/index.d.ts CHANGED
@@ -1,18 +1,18 @@
1
- import Joi from 'joi-17';
2
-
3
- declare module Joi2Json {
4
- /**
5
- * @type {string}
6
- */
7
- export type Mode = 'json' | 'open-api' | 'json-draft-2019-09' | 'json-draft-04';
8
-
9
- /**
10
- * @param {string} joi - A Joi schema.
11
- * @param {string} [mode='json'] - json / open-api / json-draft-2019-09 / json-draft-04
12
- * @param {Record} [sharedSchema={}] - Passed-in object storing shared schemas
13
- * @returns {any} Converted JSON schema object.
14
- */
15
- export function parse(joi: Joi.Schema, mode?: Mode, sharedSchema?: Record<string, any>): any;
16
- }
17
-
18
- export default Joi2Json.parse;
1
+ import Joi from 'joi-17';
2
+
3
+ declare module Joi2Json {
4
+ /**
5
+ * @type {string}
6
+ */
7
+ export type Mode = 'json' | 'open-api' | 'json-draft-2019-09' | 'json-draft-04';
8
+
9
+ /**
10
+ * @param {string} joi - A Joi schema.
11
+ * @param {string} [mode='json'] - json / open-api / json-draft-2019-09 / json-draft-04
12
+ * @param {Record} [sharedSchema={}] - Passed-in object storing shared schemas
13
+ * @returns {any} Converted JSON schema object.
14
+ */
15
+ export function parse(joi: Joi.Schema, mode?: Mode, sharedSchema?: Record<string, any>): any;
16
+ }
17
+
18
+ export default Joi2Json.parse;
package/index.js CHANGED
@@ -1,65 +1,65 @@
1
- const cmp = require('semver-compare')
2
-
3
- const c17 = require('./lib/convertors/v17')
4
- const c16 = require('./lib/convertors/v16')
5
- const c15 = require('./lib/convertors/v15')
6
- const c14 = require('./lib/convertors/v14')
7
- const c13 = require('./lib/convertors/v13')
8
- const c12 = require('./lib/convertors/v12')
9
-
10
- const JoiJsonSchemaParser = require('./lib/parsers/json')
11
- const JoiOpenApiSchemaParser = require('./lib/parsers/open-api')
12
- const JoiJsonDraftSchemaParser19 = require('./lib/parsers/json-draft-2019-09')
13
- const JoiJsonDraftSchemaParser = require('./lib/parsers/json-draft-04')
14
-
15
- const convertors = [
16
- c17, c16, c15, c14, c13, c12
17
- ]
18
- const parsers = {
19
- 'json-draft-2019-09': JoiJsonDraftSchemaParser19,
20
- 'json-draft-4': JoiJsonDraftSchemaParser,
21
- json: JoiJsonSchemaParser,
22
- 'open-api': JoiOpenApiSchemaParser
23
- }
24
-
25
- function parse(joiObj, type = 'json', definitions = {}) {
26
- if (typeof joiObj.describe !== 'function') {
27
- throw new Error('Not an joi object.')
28
- }
29
-
30
- let convertor
31
-
32
- for (let i = 0; i < convertors.length; i++) {
33
- const tmpConvertor = convertors[i]
34
- try {
35
- let version = tmpConvertor.getVersion(joiObj)
36
- let result = cmp(tmpConvertor.getSupportVersion(), version)
37
- if (result <= 0) {
38
- // The first parser has smaller or equal version
39
- convertor = tmpConvertor
40
- break
41
- }
42
- } catch (e) {
43
- // Format does not match this parser version.
44
- // Skip to check the next one
45
- continue
46
- }
47
- }
48
-
49
- if (!convertor) {
50
- console.warn('No matched joi version convertor found, using the latest version')
51
- convertor = convertors[0]
52
- }
53
-
54
- // fs.writeFileSync('./joi_spec.json', JSON.stringify(joiObj.describe(), null, 2))
55
- const joiBaseSpec = new convertor().toBaseSpec(joiObj.describe())
56
- // fs.writeFileSync(`./internal_${convertor.getSupportVersion()}_${type}.json`, JSON.stringify(joiBaseSpec, null, 2))
57
- const parser = parsers[type]
58
- if (!parser) {
59
- throw new Error(`No parser is registered for ${type}`)
60
- }
61
-
62
- return new parser().parse(joiBaseSpec, definitions)
63
- }
64
-
65
- module.exports = parse
1
+ const cmp = require('semver-compare')
2
+
3
+ const c17 = require('./lib/convertors/v17')
4
+ const c16 = require('./lib/convertors/v16')
5
+ const c15 = require('./lib/convertors/v15')
6
+ const c14 = require('./lib/convertors/v14')
7
+ const c13 = require('./lib/convertors/v13')
8
+ const c12 = require('./lib/convertors/v12')
9
+
10
+ const JoiJsonSchemaParser = require('./lib/parsers/json')
11
+ const JoiOpenApiSchemaParser = require('./lib/parsers/open-api')
12
+ const JoiJsonDraftSchemaParser19 = require('./lib/parsers/json-draft-2019-09')
13
+ const JoiJsonDraftSchemaParser = require('./lib/parsers/json-draft-04')
14
+
15
+ const convertors = [
16
+ c17, c16, c15, c14, c13, c12
17
+ ]
18
+ const parsers = {
19
+ 'json-draft-2019-09': JoiJsonDraftSchemaParser19,
20
+ 'json-draft-04': JoiJsonDraftSchemaParser,
21
+ json: JoiJsonSchemaParser,
22
+ 'open-api': JoiOpenApiSchemaParser
23
+ }
24
+
25
+ function parse(joiObj, type = 'json', definitions = {}) {
26
+ if (typeof joiObj.describe !== 'function') {
27
+ throw new Error('Not an joi object.')
28
+ }
29
+
30
+ let convertor
31
+
32
+ for (let i = 0; i < convertors.length; i++) {
33
+ const tmpConvertor = convertors[i]
34
+ try {
35
+ let version = tmpConvertor.getVersion(joiObj)
36
+ let result = cmp(tmpConvertor.getSupportVersion(), version)
37
+ if (result <= 0) {
38
+ // The first parser has smaller or equal version
39
+ convertor = tmpConvertor
40
+ break
41
+ }
42
+ } catch (e) {
43
+ // Format does not match this parser version.
44
+ // Skip to check the next one
45
+ continue
46
+ }
47
+ }
48
+
49
+ if (!convertor) {
50
+ console.warn('No matched joi version convertor found, using the latest version')
51
+ convertor = convertors[0]
52
+ }
53
+
54
+ // fs.writeFileSync('./joi_spec.json', JSON.stringify(joiObj.describe(), null, 2))
55
+ const joiBaseSpec = new convertor().toBaseSpec(joiObj.describe())
56
+ // fs.writeFileSync(`./internal_${convertor.getSupportVersion()}_${type}.json`, JSON.stringify(joiBaseSpec, null, 2))
57
+ const parser = parsers[type]
58
+ if (!parser) {
59
+ throw new Error(`No parser is registered for ${type}`)
60
+ }
61
+
62
+ return new parser().parse(joiBaseSpec, definitions)
63
+ }
64
+
65
+ module.exports = parse