Skip to content

Commit 840cc73

Browse files
authoredMar 5, 2025··
Add additionalPropertiesStrategy option to OpenApi.fromApi, close… (#4540)
1 parent 87ba23c commit 840cc73

File tree

7 files changed

+397
-44
lines changed

7 files changed

+397
-44
lines changed
 

‎.changeset/poor-years-hang.md

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
"@effect/platform": patch
3+
"effect": patch
4+
---
5+
6+
Add `additionalPropertiesStrategy` option to `OpenApi.fromApi`, closes #4531.
7+
8+
This update introduces the `additionalPropertiesStrategy` option in `OpenApi.fromApi`, allowing control over how additional properties are handled in the generated OpenAPI schema.
9+
10+
- When `"strict"` (default), additional properties are disallowed (`"additionalProperties": false`).
11+
- When `"allow"`, additional properties are allowed (`"additionalProperties": true`), making APIs more flexible.
12+
13+
The `additionalPropertiesStrategy` option has also been added to:
14+
15+
- `JSONSchema.fromAST`
16+
- `OpenApiJsonSchema.makeWithDefs`
17+
18+
**Example**
19+
20+
```ts
21+
import {
22+
HttpApi,
23+
HttpApiEndpoint,
24+
HttpApiGroup,
25+
OpenApi
26+
} from "@effect/platform"
27+
import { Schema } from "effect"
28+
29+
const api = HttpApi.make("api").add(
30+
HttpApiGroup.make("group").add(
31+
HttpApiEndpoint.get("get", "/").addSuccess(
32+
Schema.Struct({ a: Schema.String })
33+
)
34+
)
35+
)
36+
37+
const schema = OpenApi.fromApi(api, {
38+
additionalPropertiesStrategy: "allow"
39+
})
40+
41+
console.log(JSON.stringify(schema, null, 2))
42+
/*
43+
{
44+
"openapi": "3.1.0",
45+
"info": {
46+
"title": "Api",
47+
"version": "0.0.1"
48+
},
49+
"paths": {
50+
"/": {
51+
"get": {
52+
"tags": [
53+
"group"
54+
],
55+
"operationId": "group.get",
56+
"parameters": [],
57+
"security": [],
58+
"responses": {
59+
"200": {
60+
"description": "Success",
61+
"content": {
62+
"application/json": {
63+
"schema": {
64+
"type": "object",
65+
"required": [
66+
"a"
67+
],
68+
"properties": {
69+
"a": {
70+
"type": "string"
71+
}
72+
},
73+
"additionalProperties": true
74+
}
75+
}
76+
}
77+
},
78+
"400": {
79+
"description": "The request did not match the expected schema",
80+
"content": {
81+
"application/json": {
82+
"schema": {
83+
"$ref": "#/components/schemas/HttpApiDecodeError"
84+
}
85+
}
86+
}
87+
}
88+
}
89+
}
90+
}
91+
},
92+
"components": {
93+
"schemas": {
94+
"HttpApiDecodeError": {
95+
"type": "object",
96+
"required": [
97+
"issues",
98+
"message",
99+
"_tag"
100+
],
101+
"properties": {
102+
"issues": {
103+
"type": "array",
104+
"items": {
105+
"$ref": "#/components/schemas/Issue"
106+
}
107+
},
108+
"message": {
109+
"type": "string"
110+
},
111+
"_tag": {
112+
"type": "string",
113+
"enum": [
114+
"HttpApiDecodeError"
115+
]
116+
}
117+
},
118+
"additionalProperties": true,
119+
"description": "The request did not match the expected schema"
120+
},
121+
"Issue": {
122+
"type": "object",
123+
"required": [
124+
"_tag",
125+
"path",
126+
"message"
127+
],
128+
"properties": {
129+
"_tag": {
130+
"type": "string",
131+
"enum": [
132+
"Pointer",
133+
"Unexpected",
134+
"Missing",
135+
"Composite",
136+
"Refinement",
137+
"Transformation",
138+
"Type",
139+
"Forbidden"
140+
],
141+
"description": "The tag identifying the type of parse issue"
142+
},
143+
"path": {
144+
"type": "array",
145+
"items": {
146+
"$ref": "#/components/schemas/PropertyKey"
147+
},
148+
"description": "The path to the property where the issue occurred"
149+
},
150+
"message": {
151+
"type": "string",
152+
"description": "A descriptive message explaining the issue"
153+
}
154+
},
155+
"additionalProperties": true,
156+
"description": "Represents an error encountered while parsing a value to match the schema"
157+
},
158+
"PropertyKey": {
159+
"anyOf": [
160+
{
161+
"type": "string"
162+
},
163+
{
164+
"type": "number"
165+
},
166+
{
167+
"type": "object",
168+
"required": [
169+
"_tag",
170+
"key"
171+
],
172+
"properties": {
173+
"_tag": {
174+
"type": "string",
175+
"enum": [
176+
"symbol"
177+
]
178+
},
179+
"key": {
180+
"type": "string"
181+
}
182+
},
183+
"additionalProperties": true,
184+
"description": "an object to be decoded into a globally shared symbol"
185+
}
186+
]
187+
}
188+
},
189+
"securitySchemes": {}
190+
},
191+
"security": [],
192+
"tags": [
193+
{
194+
"name": "group"
195+
}
196+
]
197+
}
198+
*/
199+
```

‎packages/effect/src/JSONSchema.ts

+55-15
Original file line numberDiff line numberDiff line change
@@ -271,14 +271,16 @@ type Target = "jsonSchema7" | "jsonSchema2019-09" | "openApi3.1"
271271

272272
type TopLevelReferenceStrategy = "skip" | "keep"
273273

274+
type AdditionalPropertiesStrategy = "allow" | "strict"
275+
274276
/**
275277
* Returns a JSON Schema with additional options and definitions.
276278
*
277279
* **Warning**
278280
*
279281
* This function is experimental and subject to change.
280282
*
281-
* **Details**
283+
* **Options**
282284
*
283285
* - `definitions`: A record of definitions that are included in the schema.
284286
* - `definitionPath`: The path to the definitions within the schema (defaults
@@ -291,24 +293,30 @@ type TopLevelReferenceStrategy = "skip" | "keep"
291293
* reference. Possible values are:
292294
* - `"keep"`: Keep the top-level reference (default behavior).
293295
* - `"skip"`: Skip the top-level reference.
296+
* - `additionalPropertiesStrategy`: Controls the handling of additional properties. Possible values are:
297+
* - `"strict"`: Disallow additional properties (default behavior).
298+
* - `"allow"`: Allow additional properties.
294299
*
295300
* @category encoding
296301
* @since 3.11.5
297302
* @experimental
298303
*/
299304
export const fromAST = (ast: AST.AST, options: {
300305
readonly definitions: Record<string, JsonSchema7>
301-
readonly definitionPath?: string
302-
readonly target?: Target
303-
readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy
306+
readonly definitionPath?: string | undefined
307+
readonly target?: Target | undefined
308+
readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined
309+
readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined
304310
}): JsonSchema7 => {
305311
const definitionPath = options.definitionPath ?? "#/$defs/"
306312
const getRef = (id: string) => definitionPath + id
307-
const target: Target = options.target ?? "jsonSchema7"
313+
const target = options.target ?? "jsonSchema7"
308314
const handleIdentifier = options.topLevelReferenceStrategy !== "skip"
315+
const additionalPropertiesStrategy = options.additionalPropertiesStrategy ?? "strict"
309316
return go(ast, options.definitions, handleIdentifier, [], {
310317
getRef,
311-
target
318+
target,
319+
additionalPropertiesStrategy
312320
})
313321
}
314322

@@ -450,19 +458,51 @@ const mergeRefinements = (from: any, jsonSchema: any, annotations: any): any =>
450458
return out
451459
}
452460

453-
type Options = {
461+
type GoOptions = {
454462
readonly getRef: (id: string) => string
455463
readonly target: Target
464+
readonly additionalPropertiesStrategy: AdditionalPropertiesStrategy
456465
}
457466

458-
type Path = ReadonlyArray<PropertyKey>
459-
460-
const isContentSchemaSupported = (options: Options) => options.target !== "jsonSchema7"
467+
function isContentSchemaSupported(options: GoOptions): boolean {
468+
switch (options.target) {
469+
case "jsonSchema7":
470+
return false
471+
case "jsonSchema2019-09":
472+
case "openApi3.1":
473+
return true
474+
}
475+
}
461476

462-
const isNullTypeKeywordSupported = (options: Options) => options.target !== "openApi3.1"
477+
function isNullTypeKeywordSupported(options: GoOptions): boolean {
478+
switch (options.target) {
479+
case "jsonSchema7":
480+
case "jsonSchema2019-09":
481+
return true
482+
case "openApi3.1":
483+
return false
484+
}
485+
}
463486

464487
// https://swagger.io/docs/specification/v3_0/data-models/data-types/#null
465-
const isNullableKeywordSupported = (options: Options) => options.target === "openApi3.1"
488+
function isNullableKeywordSupported(options: GoOptions): boolean {
489+
switch (options.target) {
490+
case "jsonSchema7":
491+
case "jsonSchema2019-09":
492+
return false
493+
case "openApi3.1":
494+
return true
495+
}
496+
}
497+
498+
function getAdditionalProperties(options: GoOptions): boolean {
499+
switch (options.additionalPropertiesStrategy) {
500+
case "allow":
501+
return true
502+
case "strict":
503+
return false
504+
}
505+
}
466506

467507
const isNeverJSONSchema = (jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Never =>
468508
"$id" in jsonSchema && jsonSchema.$id === "/schemas/never"
@@ -496,8 +536,8 @@ const go = (
496536
ast: AST.AST,
497537
$defs: Record<string, JsonSchema7>,
498538
handleIdentifier: boolean,
499-
path: Path,
500-
options: Options
539+
path: ReadonlyArray<PropertyKey>,
540+
options: GoOptions
501541
): JsonSchema7 => {
502542
if (handleIdentifier) {
503543
const identifier = AST.getJSONIdentifier(ast)
@@ -639,7 +679,7 @@ const go = (
639679
type: "object",
640680
required: [],
641681
properties: {},
642-
additionalProperties: false
682+
additionalProperties: getAdditionalProperties(options)
643683
}
644684
let patternProperties: JsonSchema7 | undefined = undefined
645685
let propertyNames: JsonSchema7 | undefined = undefined

‎packages/effect/test/Schema/JSONSchema.test.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const expectError = <A, I>(schema: Schema.Schema<A, I>, message: string) => {
103103
// Using this instead of Schema.JsonNumber to avoid cluttering the output with unnecessary description and title
104104
const JsonNumber = Schema.Number.pipe(Schema.filter((n) => Number.isFinite(n), { jsonSchema: {} }))
105105

106-
describe("makeWithOptions", () => {
106+
describe("fromAST", () => {
107107
it("definitionsPath", () => {
108108
const schema = Schema.String.annotations({ identifier: "08368672-2c02-4d6d-92b0-dd0019b33a7b" })
109109
const definitions = {}
@@ -601,6 +601,28 @@ describe("makeWithOptions", () => {
601601
deepStrictEqual(definitions, {})
602602
})
603603
})
604+
605+
describe("additionalPropertiesStrategy", () => {
606+
it(`"allow"`, () => {
607+
const schema = Schema.Struct({ a: Schema.String })
608+
const definitions = {}
609+
const jsonSchema = JSONSchema.fromAST(schema.ast, {
610+
definitions,
611+
additionalPropertiesStrategy: "allow"
612+
})
613+
deepStrictEqual(jsonSchema, {
614+
"type": "object",
615+
"properties": {
616+
"a": {
617+
"type": "string"
618+
}
619+
},
620+
"required": ["a"],
621+
"additionalProperties": true
622+
})
623+
deepStrictEqual(definitions, {})
624+
})
625+
})
604626
})
605627

606628
describe("make", () => {

‎packages/platform/src/OpenApi.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ function processAnnotation<Services, S, I>(
175175
}
176176
}
177177

178+
type AdditionalPropertiesStrategy = "allow" | "strict"
179+
178180
/**
179181
* Converts an `HttpApi` instance into an OpenAPI Specification object.
180182
*
@@ -192,6 +194,12 @@ function processAnnotation<Services, S, I>(
192194
* and overrides. Cached results are used for better performance when the same
193195
* `HttpApi` instance is processed multiple times.
194196
*
197+
* **Options**
198+
*
199+
* - `additionalPropertiesStrategy`: Controls the handling of additional properties. Possible values are:
200+
* - `"strict"`: Disallow additional properties (default behavior).
201+
* - `"allow"`: Allow additional properties.
202+
*
195203
* @example
196204
* ```ts
197205
* import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform"
@@ -214,7 +222,10 @@ function processAnnotation<Services, S, I>(
214222
* @since 1.0.0
215223
*/
216224
export const fromApi = <Id extends string, Groups extends HttpApiGroup.Any, E, R>(
217-
api: HttpApi.HttpApi<Id, Groups, E, R>
225+
api: HttpApi.HttpApi<Id, Groups, E, R>,
226+
options?: {
227+
readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined
228+
} | undefined
218229
): OpenAPISpec => {
219230
const cached = apiCache.get(api)
220231
if (cached !== undefined) {
@@ -238,7 +249,8 @@ export const fromApi = <Id extends string, Groups extends HttpApiGroup.Any, E, R
238249

239250
function processAST(ast: AST.AST): JsonSchema.JsonSchema {
240251
return JsonSchema.fromAST(ast, {
241-
defs: jsonSchemaDefs
252+
defs: jsonSchemaDefs,
253+
additionalPropertiesStrategy: options?.additionalPropertiesStrategy
242254
})
243255
}
244256

‎packages/platform/src/OpenApiJsonSchema.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -255,35 +255,47 @@ export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): Root => {
255255
return out
256256
}
257257

258+
type TopLevelReferenceStrategy = "skip" | "keep"
259+
260+
type AdditionalPropertiesStrategy = "allow" | "strict"
261+
258262
/**
259263
* Creates a schema with additional options and definitions.
260264
*
265+
* **Options**
266+
*
261267
* - `defs`: A record of definitions that are included in the schema.
262268
* - `defsPath`: The path to the definitions within the schema (defaults to "#/$defs/").
263269
* - `topLevelReferenceStrategy`: Controls the handling of the top-level reference. Possible values are:
264270
* - `"keep"`: Keep the top-level reference (default behavior).
265271
* - `"skip"`: Skip the top-level reference.
272+
* - `additionalPropertiesStrategy`: Controls the handling of additional properties. Possible values are:
273+
* - `"strict"`: Disallow additional properties (default behavior).
274+
* - `"allow"`: Allow additional properties.
266275
*
267276
* @category encoding
268277
* @since 1.0.0
269278
*/
270279
export const makeWithDefs = <A, I, R>(schema: Schema.Schema<A, I, R>, options: {
271280
readonly defs: Record<string, any>
272-
readonly defsPath?: string
273-
readonly topLevelReferenceStrategy?: "skip" | "keep"
281+
readonly defsPath?: string | undefined
282+
readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined
283+
readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined
274284
}): JsonSchema => fromAST(schema.ast, options)
275285

276286
/** @internal */
277287
export const fromAST = (ast: AST.AST, options: {
278288
readonly defs: Record<string, any>
279-
readonly defsPath?: string
280-
readonly topLevelReferenceStrategy?: "skip" | "keep"
289+
readonly defsPath?: string | undefined
290+
readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined
291+
readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined
281292
}): JsonSchema => {
282293
const jsonSchema = JSONSchema.fromAST(ast, {
283294
definitions: options.defs,
284295
definitionPath: options.defsPath ?? "#/components/schemas/",
285296
target: "openApi3.1",
286-
topLevelReferenceStrategy: options.topLevelReferenceStrategy ?? "keep"
297+
topLevelReferenceStrategy: options.topLevelReferenceStrategy,
298+
additionalPropertiesStrategy: options.additionalPropertiesStrategy
287299
})
288300
return jsonSchema as JsonSchema
289301
}

‎packages/platform/test/OpenApi.test.ts

+69-21
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,27 @@ const HttpApiDecodeError = {
2424
}
2525
}
2626

27-
type Options = {
27+
type Parts = {
2828
readonly paths: OpenApi.OpenAPISpec["paths"]
2929
readonly tags?: OpenApi.OpenAPISpec["tags"] | undefined
3030
readonly securitySchemes?: Record<string, OpenApi.OpenAPISecurityScheme> | undefined
3131
readonly schemas?: Record<string, OpenApiJsonSchema.JsonSchema> | undefined
3232
readonly security?: Array<OpenApi.OpenAPISecurityRequirement> | undefined
3333
}
3434

35-
const getSpec = (options: Options): OpenApi.OpenAPISpec => {
35+
type AdditionalPropertiesStrategy = "allow" | "strict"
36+
37+
type Options = {
38+
readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined
39+
}
40+
41+
const getSpec = (parts: Parts, options?: Options): OpenApi.OpenAPISpec => {
42+
const additionalPropertiesStrategy = (options?.additionalPropertiesStrategy ?? "strict") === "allow"
3643
return {
3744
"openapi": "3.1.0",
3845
"info": { "title": "Api", "version": "0.0.1" },
39-
"paths": options.paths,
40-
"tags": options.tags ?? [{ "name": "group" }],
46+
"paths": parts.paths,
47+
"tags": parts.tags ?? [{ "name": "group" }],
4148
"components": {
4249
"schemas": {
4350
"HttpApiDecodeError": {
@@ -58,7 +65,7 @@ const getSpec = (options: Options): OpenApi.OpenAPISpec => {
5865
]
5966
}
6067
},
61-
"additionalProperties": false,
68+
"additionalProperties": additionalPropertiesStrategy,
6269
"description": "The request did not match the expected schema"
6370
},
6471
"Issue": {
@@ -92,14 +99,14 @@ const getSpec = (options: Options): OpenApi.OpenAPISpec => {
9299
"description": "A descriptive message explaining the issue"
93100
}
94101
},
95-
"additionalProperties": false
102+
"additionalProperties": additionalPropertiesStrategy
96103
},
97104
"PropertyKey": {
98105
"anyOf": [
99106
{ "type": "string" },
100107
{ "type": "number" },
101108
{
102-
"additionalProperties": false,
109+
"additionalProperties": additionalPropertiesStrategy,
103110
"description": "an object to be decoded into a globally shared symbol",
104111
"properties": {
105112
"_tag": {
@@ -115,33 +122,35 @@ const getSpec = (options: Options): OpenApi.OpenAPISpec => {
115122
}
116123
]
117124
},
118-
...options.schemas
125+
...parts.schemas
119126
},
120-
"securitySchemes": options.securitySchemes ?? {}
127+
"securitySchemes": parts.securitySchemes ?? {}
121128
},
122-
"security": options.security ?? []
129+
"security": parts.security ?? []
123130
}
124131
}
125132

126-
const expectOptions = <Id extends string, Groups extends HttpApiGroup.HttpApiGroup.Any, E, R>(
133+
const expectParts = <Id extends string, Groups extends HttpApiGroup.HttpApiGroup.Any, E, R>(
127134
api: HttpApi.HttpApi<Id, Groups, E, R>,
128-
options: Options
135+
parts: Parts
129136
) => {
130-
expectSpec(api, getSpec(options))
137+
expectSpec(api, getSpec(parts))
131138
}
132139

133140
const expectSpecPaths = <Id extends string, Groups extends HttpApiGroup.HttpApiGroup.Any, E, R>(
134141
api: HttpApi.HttpApi<Id, Groups, E, R>,
135-
paths: OpenApi.OpenAPISpec["paths"]
142+
paths: OpenApi.OpenAPISpec["paths"],
143+
options?: Options
136144
) => {
137-
expectSpec(api, getSpec({ paths }))
145+
expectSpec(api, getSpec({ paths }, options), options)
138146
}
139147

140148
const expectSpec = <Id extends string, Groups extends HttpApiGroup.HttpApiGroup.Any, E, R>(
141149
api: HttpApi.HttpApi<Id, Groups, E, R>,
142-
expected: OpenApi.OpenAPISpec
150+
expected: OpenApi.OpenAPISpec,
151+
options?: Options
143152
) => {
144-
const spec = OpenApi.fromApi(api)
153+
const spec = OpenApi.fromApi(api, options)
145154
// console.log(JSON.stringify(spec.paths, null, 2))
146155
// console.log(JSON.stringify(spec, null, 2))
147156
deepStrictEqual(spec, expected)
@@ -158,6 +167,45 @@ const expectPaths = <Id extends string, Groups extends HttpApiGroup.HttpApiGroup
158167
describe("OpenApi", () => {
159168
describe("fromApi", () => {
160169
describe("HttpApi", () => {
170+
it(`additionalPropertiesStrategy: "allow"`, () => {
171+
const api = HttpApi.make("api").add(
172+
HttpApiGroup.make("group").add(
173+
HttpApiEndpoint.get("get", "/")
174+
.addSuccess(Schema.Struct({ a: Schema.String }))
175+
)
176+
)
177+
expectSpecPaths(api, {
178+
"/": {
179+
"get": {
180+
"tags": ["group"],
181+
"operationId": "group.get",
182+
"parameters": [],
183+
"security": [],
184+
"responses": {
185+
"200": {
186+
"description": "Success",
187+
"content": {
188+
"application/json": {
189+
"schema": {
190+
"type": "object",
191+
"properties": {
192+
"a": {
193+
"type": "string"
194+
}
195+
},
196+
"required": ["a"],
197+
"additionalProperties": true
198+
}
199+
}
200+
}
201+
},
202+
"400": HttpApiDecodeError
203+
}
204+
}
205+
}
206+
}, { additionalPropertiesStrategy: "allow" })
207+
})
208+
161209
it("addHttpApi", () => {
162210
const anotherApi = HttpApi.make("api").add(
163211
HttpApiGroup.make("group").add(
@@ -810,7 +858,7 @@ describe("OpenApi", () => {
810858
.addSuccess(User)
811859
)
812860
)
813-
expectOptions(api, {
861+
expectParts(api, {
814862
schemas: {
815863
"User": {
816864
"additionalProperties": false,
@@ -1534,7 +1582,7 @@ describe("OpenApi", () => {
15341582
)
15351583
// Or apply the middleware to the entire API
15361584
.middleware(Authorization)
1537-
expectOptions(api, {
1585+
expectParts(api, {
15381586
security: [{
15391587
"myBearer": []
15401588
}, {
@@ -1745,7 +1793,7 @@ describe("OpenApi", () => {
17451793
)
17461794
)
17471795
)
1748-
expectOptions(api, {
1796+
expectParts(api, {
17491797
schemas: {
17501798
"PersistedFile": {
17511799
"type": "string",
@@ -2081,7 +2129,7 @@ describe("OpenApi", () => {
20812129
.addError(err).addError(err)
20822130
).addError(err).addError(err)
20832131
).addError(err).addError(err)
2084-
expectOptions(api, {
2132+
expectParts(api, {
20852133
tags: [{ name: "group1" }, { name: "group2" }],
20862134
schemas: {
20872135
"err": {

‎packages/platform/test/OpenApiJsonSchema.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,24 @@ describe("OpenApiJsonSchema", () => {
5353
}
5454
})
5555
})
56+
57+
it(`additionalPropertiesStrategy: "allow"`, () => {
58+
const schema = Schema.Struct({ a: Schema.String })
59+
const defs: Record<string, OpenApiJsonSchema.JsonSchema> = {}
60+
const jsonSchema = OpenApiJsonSchema.makeWithDefs(schema, {
61+
defs,
62+
additionalPropertiesStrategy: "allow"
63+
})
64+
deepStrictEqual(jsonSchema, {
65+
"type": "object",
66+
"properties": {
67+
"a": {
68+
"type": "string"
69+
}
70+
},
71+
"required": ["a"],
72+
"additionalProperties": true
73+
})
74+
deepStrictEqual(defs, {})
75+
})
5676
})

0 commit comments

Comments
 (0)
Please sign in to comment.