diff --git a/docs/.vuepress/public/vite-plugin-ssr-tsed.png b/docs/.vuepress/public/vite-tsed.png
similarity index 100%
rename from docs/.vuepress/public/vite-plugin-ssr-tsed.png
rename to docs/.vuepress/public/vite-tsed.png
diff --git a/docs/docs/model.md b/docs/docs/model.md
index 81aac30f5b3..ce52f56dfd9 100644
--- a/docs/docs/model.md
+++ b/docs/docs/model.md
@@ -112,6 +112,131 @@ json type or when you use a mixed TypeScript types.
+You can also use @@Any@@ decorator to allow all types:
+
+
+
+
+```typescript
+import {Any} from "@tsed/schema";
+
+export class Model {
+ @Any()
+ prop: any;
+}
+```
+
+
+
+
+```json
+{
+ "properties": {
+ "prop": {
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "integer",
+ "multipleOf": 1
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "type": "object"
+}
+```
+
+
+
+
+```json
+{
+ "properties": {
+ "prop": {
+ "nullable": true,
+ "anyOf": [
+ {
+ "type": "integer",
+ "multipleOf": 1
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "type": "object"
+}
+```
+
+
+
+
+Since v7.75.0, when you use @@Any@@ decorator combined with other decorators like @@MinLength@@, @@Minimum@@, etc. metadata will be automatically assigned to the right
+type. For example, if you add a @@Minimum@@ decorator, it will be assigned to the number type.
+
+```ts
+import {Any} from "@tsed/schema";
+
+class Model {
+ @Any(String, Number)
+ @Minimum(0)
+ @MaxLength(100)
+ prop: string | number;
+}
+```
+
+Produce a json-schema as follows:
+
+```json
+{
+ "properties": {
+ "prop": {
+ "allOf": [
+ {
+ "type": "string",
+ "maxLength": 100
+ },
+ {
+ "type": "number",
+ "minimum": 0
+ }
+ ]
+ }
+ },
+ "type": "object"
+}
+```
+
## Nullable
The @@Nullable@@ decorator is used allow a null value on a field while preserving the original Typescript type.
@@ -189,62 +314,50 @@ class NullableModel {
`returnsCoercedValue` will become true by default in the next major version of Ts.ED.
:::
-## Any
-
-The @@Any@@ decorator is used to allow any types:
+## Nullable and mixed types
-
-
+The @@Nullable@@ decorator can be used with Tuple types:
-```typescript
-import {Any} from "@tsed/schema";
+```ts
+import {Nullable} from "@tsed/schema";
-export class Model {
- @Any()
- prop: any;
+class Model {
+ @Nullable(String, Number)
+ prop: string | number | null;
}
```
-
-
+Since v7.75.0, when you use @@Nullable@@ decorator combined with other decorators like @@MinLength@@, @@Minimum@@, etc. metadata will be automatically assigned to the right
+type. For example, if you add a @@Minimum@@ decorator, it will be assigned to the number type.
-```json
-{
- "properties": {
- "prop": {
- "type": ["integer", "number", "string", "boolean", "array", "object", "null"]
- }
- },
- "type": "object"
+```ts
+import {Nullable} from "@tsed/schema";
+
+class Model {
+ @Nullable(String, Number)
+ @Minimum(0)
+ @MaxLength(100)
+ prop: string | number | null;
}
```
-
-
+Produce a json-schema as follows:
```json
{
"properties": {
"prop": {
- "nullable": true,
- "oneOf": [
+ "anyOf": [
{
- "type": "integer"
+ "type": "null"
},
{
- "type": "number"
+ "type": "string",
+ "maxLength": 100
},
{
- "type": "string"
- },
- {
- "type": "boolean"
- },
- {
- "type": "array"
- },
- {
- "type": "object"
+ "type": "number",
+ "minimum": 0
}
]
}
@@ -253,9 +366,6 @@ export class Model {
}
```
-
-
-
## Regular expressions
The @@Pattern@@ decorator is used to restrict a string to a particular regular expression. The regular expression syntax
diff --git a/docs/docs/snippets/model/any-types.json b/docs/docs/snippets/model/any-types.json
index cc0cd5deca6..31c463b5878 100644
--- a/docs/docs/snippets/model/any-types.json
+++ b/docs/docs/snippets/model/any-types.json
@@ -1,14 +1,53 @@
{
- "definitions": {},
"properties": {
"prop1": {
- "type": ["integer", "number", "string", "boolean", "array", "object", "null"]
+ "allOf": [
+ {
+ "type": "integer",
+ "minLength": 1
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
"prop2": {
- "type": ["string", "number", "boolean"]
+ "allOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ }
+ ]
},
"prop3": {
- "type": ["string", "null"]
+ "allOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
},
"type": "object"
diff --git a/packages/orm/mongoose/src/decorators/virtualRef.spec.ts b/packages/orm/mongoose/src/decorators/virtualRef.spec.ts
index 8e20073a277..ebe167ac8fa 100644
--- a/packages/orm/mongoose/src/decorators/virtualRef.spec.ts
+++ b/packages/orm/mongoose/src/decorators/virtualRef.spec.ts
@@ -201,7 +201,7 @@ describe("@VirtualRef()", () => {
type: "number"
},
members: {
- oneOf: [
+ anyOf: [
{
$ref: "#/components/schemas/TestPerson"
}
diff --git a/packages/orm/mongoose/test/enums.integration.spec.ts b/packages/orm/mongoose/test/enums.integration.spec.ts
index 4b68915e9ad..1856a7a3206 100644
--- a/packages/orm/mongoose/test/enums.integration.spec.ts
+++ b/packages/orm/mongoose/test/enums.integration.spec.ts
@@ -47,8 +47,7 @@ describe("Enums integration", () => {
ResponseTimeThreshold: {
properties: {
status: {
- $ref: "#/definitions/ComponentStatuses",
- minLength: 1
+ $ref: "#/definitions/ComponentStatuses"
}
},
required: ["status"],
diff --git a/packages/platform/platform-express/test/__snapshots__/array-body.spec.ts.snap b/packages/platform/platform-express/test/__snapshots__/array-body.spec.ts.snap
index 201be2cd67f..68a68f192dc 100644
--- a/packages/platform/platform-express/test/__snapshots__/array-body.spec.ts.snap
+++ b/packages/platform/platform-express/test/__snapshots__/array-body.spec.ts.snap
@@ -29,9 +29,9 @@ Object {
"application/json": Object {
"schema": Object {
"items": Object {
- "nullable": true,
- "oneOf": Array [
+ "anyOf": Array [
Object {
+ "multipleOf": 1,
"type": "integer",
},
Object {
@@ -50,6 +50,7 @@ Object {
"type": "object",
},
],
+ "nullable": true,
},
"type": "array",
},
@@ -76,9 +77,9 @@ Object {
"application/json": Object {
"schema": Object {
"items": Object {
- "nullable": true,
- "oneOf": Array [
+ "anyOf": Array [
Object {
+ "multipleOf": 1,
"type": "integer",
},
Object {
@@ -97,6 +98,7 @@ Object {
"type": "object",
},
],
+ "nullable": true,
},
"type": "array",
},
@@ -200,9 +202,9 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
- "nullable": true,
- "oneOf": Array [
+ "anyOf": Array [
Object {
+ "multipleOf": 1,
"type": "integer",
},
Object {
@@ -221,6 +223,7 @@ Object {
"type": "object",
},
],
+ "nullable": true,
},
},
},
diff --git a/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap b/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap
index f8c3ef4eae1..e95b0b89011 100644
--- a/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap
+++ b/packages/platform/platform-express/test/__snapshots__/discriminator.spec.ts.snap
@@ -30,6 +30,7 @@ Object {
"ActionPartial": Object {
"properties": Object {
"event": Object {
+ "minLength": 1,
"type": "string",
},
"type": Object {
@@ -71,6 +72,7 @@ Object {
"CustomActionPartial": Object {
"properties": Object {
"event": Object {
+ "minLength": 1,
"type": "string",
},
"meta": Object {
@@ -112,6 +114,7 @@ Object {
"type": "string",
},
"url": Object {
+ "minLength": 1,
"type": "string",
},
"value": Object {
@@ -203,7 +206,6 @@ Object {
"discriminator": Object {
"propertyName": "type",
},
- "nullable": true,
"oneOf": Array [
Object {
"$ref": "#/components/schemas/PageView",
diff --git a/packages/platform/platform-params/src/decorators/useType.ts b/packages/platform/platform-params/src/decorators/useType.ts
index 6067ab9dcde..7b2b8c6fe55 100644
--- a/packages/platform/platform-params/src/decorators/useType.ts
+++ b/packages/platform/platform-params/src/decorators/useType.ts
@@ -1,7 +1,15 @@
import {Type} from "@tsed/core";
-import {Any, CollectionOf} from "@tsed/schema";
+import {Any, CollectionOf, type JsonParameterStore} from "@tsed/schema";
import {ParamFn} from "./paramFn.js";
+function shouldFallBackToAny(entity: JsonParameterStore) {
+ if (entity.itemSchema.has("allOf") || entity.itemSchema.has("anyOf") || entity.itemSchema.has("oneOf")) {
+ return false;
+ }
+
+ return entity.isCollection && entity.type === Object && [undefined, "object"].includes(entity.itemSchema.get("type"));
+}
+
/**
* Set the type of the item collection.
*
@@ -19,7 +27,7 @@ export function UseType(useType: undefined | any | Type) {
return CollectionOf(useType);
}
- if (entity.isCollection && entity.type === Object && [undefined, "object"].includes(entity.itemSchema.get("type"))) {
+ if (shouldFallBackToAny(entity)) {
Any()(...parameters);
}
});
diff --git a/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts b/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts
index 9efb8452e0a..fc8c8f795ba 100644
--- a/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts
+++ b/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts
@@ -1,6 +1,6 @@
import {PlatformTest, Post} from "@tsed/common";
import {catchAsyncError} from "@tsed/core";
-import {CollectionOf, getSpec, JsonParameterStore, Required, SpecTypes} from "@tsed/schema";
+import {AllOf, AnyOf, CollectionOf, getSpec, JsonParameterStore, OneOf, Property, Required, SpecTypes} from "@tsed/schema";
import {BodyParams} from "../decorators/bodyParams.js";
import {PathParams} from "../decorators/pathParams.js";
import {QueryParams} from "../decorators/queryParams.js";
@@ -61,6 +61,342 @@ describe("ValidationPipe", () => {
});
expect(result).toEqual("value");
});
+ it("should return value (Body with array param)", async () => {
+ const validator = await PlatformTest.invoke(ValidationPipe);
+ // @ts-ignore
+ validator.validator = undefined;
+
+ class Test {
+ @Post("/")
+ test(@BodyParams() type: any[]) {}
+ }
+
+ // WHEN
+ const param = JsonParameterStore.get(Test, "test", 0);
+ const result = await validator.transform("value", param);
+
+ // THEN
+ expect(getSpec(Test, {specType: SpecTypes.OPENAPI})).toEqual({
+ paths: {
+ "/": {
+ post: {
+ operationId: "testTest",
+ parameters: [],
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ items: {
+ anyOf: [
+ {
+ multipleOf: 1,
+ type: "integer"
+ },
+ {
+ type: "number"
+ },
+ {
+ type: "string"
+ },
+ {
+ type: "boolean"
+ },
+ {
+ type: "array"
+ },
+ {
+ type: "object"
+ }
+ ],
+ nullable: true
+ },
+ type: "array"
+ }
+ }
+ },
+ required: false
+ },
+ responses: {
+ "200": {
+ description: "Success"
+ }
+ },
+ tags: ["Test"]
+ }
+ }
+ },
+ tags: [
+ {
+ name: "Test"
+ }
+ ]
+ });
+ expect(result).toEqual("value");
+ });
+ it("should return value (Body with array oneOf)", async () => {
+ const validator = await PlatformTest.invoke(ValidationPipe);
+ // @ts-ignore
+ validator.validator = undefined;
+
+ class Model {
+ @Property()
+ id: string;
+ }
+
+ class Model2 {
+ @Property()
+ id: string;
+ }
+
+ class Test {
+ @Post("/")
+ test(@BodyParams() @OneOf(Model, Model2) type: any[]) {}
+ }
+
+ // WHEN
+ const param = JsonParameterStore.get(Test, "test", 0);
+ const result = await validator.transform("value", param);
+
+ // THEN
+ expect(getSpec(Test, {specType: SpecTypes.OPENAPI})).toEqual({
+ components: {
+ schemas: {
+ Model: {
+ properties: {
+ id: {
+ type: "string"
+ }
+ },
+ type: "object"
+ },
+ Model2: {
+ properties: {
+ id: {
+ type: "string"
+ }
+ },
+ type: "object"
+ }
+ }
+ },
+ paths: {
+ "/": {
+ post: {
+ operationId: "testTest",
+ parameters: [],
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ items: {
+ oneOf: [
+ {
+ $ref: "#/components/schemas/Model"
+ },
+ {
+ $ref: "#/components/schemas/Model2"
+ }
+ ]
+ },
+ type: "array"
+ }
+ }
+ },
+ required: false
+ },
+ responses: {
+ "200": {
+ description: "Success"
+ }
+ },
+ tags: ["Test"]
+ }
+ }
+ },
+ tags: [
+ {
+ name: "Test"
+ }
+ ]
+ });
+ expect(result).toEqual("value");
+ });
+ it("should return value (Body with array anyOf)", async () => {
+ const validator = await PlatformTest.invoke(ValidationPipe);
+ // @ts-ignore
+ validator.validator = undefined;
+
+ class Model {
+ @Property()
+ id: string;
+ }
+
+ class Model2 {
+ @Property()
+ id: string;
+ }
+
+ class Test {
+ @Post("/")
+ test(@BodyParams() @AnyOf(Model, Model2) type: any[]) {}
+ }
+
+ // WHEN
+ const param = JsonParameterStore.get(Test, "test", 0);
+ const result = await validator.transform("value", param);
+
+ // THEN
+ expect(getSpec(Test, {specType: SpecTypes.OPENAPI})).toEqual({
+ components: {
+ schemas: {
+ Model: {
+ properties: {
+ id: {
+ type: "string"
+ }
+ },
+ type: "object"
+ },
+ Model2: {
+ properties: {
+ id: {
+ type: "string"
+ }
+ },
+ type: "object"
+ }
+ }
+ },
+ paths: {
+ "/": {
+ post: {
+ operationId: "testTest",
+ parameters: [],
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ items: {
+ anyOf: [
+ {
+ $ref: "#/components/schemas/Model"
+ },
+ {
+ $ref: "#/components/schemas/Model2"
+ }
+ ]
+ },
+ type: "array"
+ }
+ }
+ },
+ required: false
+ },
+ responses: {
+ "200": {
+ description: "Success"
+ }
+ },
+ tags: ["Test"]
+ }
+ }
+ },
+ tags: [
+ {
+ name: "Test"
+ }
+ ]
+ });
+ expect(result).toEqual("value");
+ });
+ it("should return value (Body with array allOf)", async () => {
+ const validator = await PlatformTest.invoke(ValidationPipe);
+ // @ts-ignore
+ validator.validator = undefined;
+
+ class Model {
+ @Property()
+ id: string;
+ }
+
+ class Model2 {
+ @Property()
+ id: string;
+ }
+
+ class Test {
+ @Post("/")
+ test(@BodyParams() @AllOf(Model, Model2) type: any[]) {}
+ }
+
+ // WHEN
+ const param = JsonParameterStore.get(Test, "test", 0);
+ const result = await validator.transform("value", param);
+
+ // THEN
+ expect(getSpec(Test, {specType: SpecTypes.OPENAPI})).toEqual({
+ components: {
+ schemas: {
+ Model: {
+ properties: {
+ id: {
+ type: "string"
+ }
+ },
+ type: "object"
+ },
+ Model2: {
+ properties: {
+ id: {
+ type: "string"
+ }
+ },
+ type: "object"
+ }
+ }
+ },
+ paths: {
+ "/": {
+ post: {
+ operationId: "testTest",
+ parameters: [],
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ items: {
+ allOf: [
+ {
+ $ref: "#/components/schemas/Model"
+ },
+ {
+ $ref: "#/components/schemas/Model2"
+ }
+ ]
+ },
+ type: "array"
+ }
+ }
+ },
+ required: false
+ },
+ responses: {
+ "200": {
+ description: "Success"
+ }
+ },
+ tags: ["Test"]
+ }
+ }
+ },
+ tags: [
+ {
+ name: "Test"
+ }
+ ]
+ });
+ expect(result).toEqual("value");
+ });
it("should skip validation for array path params", async () => {
const validator = await PlatformTest.invoke(ValidationPipe);
// @ts-ignore
@@ -206,7 +542,11 @@ describe("ValidationPipe", () => {
// THEN
expect(result).toEqual("1");
// @ts-ignore
- expect(validator.validator.validate).toHaveBeenCalledWith("1", {collectionType: undefined, schema: {type: "string"}, type: undefined});
+ expect(validator.validator.validate).toHaveBeenCalledWith("1", {
+ collectionType: undefined,
+ schema: {type: "string", minLength: 1},
+ type: undefined
+ });
});
it("should cast string to array", async () => {
const validator = await PlatformTest.invoke(ValidationPipe);
diff --git a/packages/specs/schema/jest.config.js b/packages/specs/schema/jest.config.js
index 178c8afed95..35253a72613 100644
--- a/packages/specs/schema/jest.config.js
+++ b/packages/specs/schema/jest.config.js
@@ -8,7 +8,7 @@ module.exports = {
coverageThreshold: {
global: {
statements: 99.41,
- branches: 96.21,
+ branches: 96.07,
functions: 100,
lines: 99.41
}
diff --git a/packages/specs/schema/src/constants/jsonSchemaProperties.ts b/packages/specs/schema/src/constants/jsonSchemaProperties.ts
new file mode 100644
index 00000000000..004d845a6ca
--- /dev/null
+++ b/packages/specs/schema/src/constants/jsonSchemaProperties.ts
@@ -0,0 +1,17 @@
+export const MANY_OF_PROPERTIES = ["oneOf", "allOf", "anyOf"];
+export const STRING_PROPERTIES = ["minLength", "maxLength", "pattern", "format"];
+export const BOOLEAN_PROPERTIES = [];
+export const NUMBER_PROPERTIES = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"];
+export const ARRAY_PROPERTIES = ["maxItems", "minItems", "uniqueItems", "items", "contains", "maxContains", "minContains"];
+export const OBJECT_PROPERTIES = [
+ "maxItems",
+ "minItems",
+ "uniqueItems",
+ "items",
+ "contains",
+ "maxContains",
+ "minContains",
+ "patternProperties",
+ "dependencies"
+];
+export const COMMON_PROPERTIES = ["const", "enum"];
diff --git a/packages/specs/schema/src/decorators/common/allow.spec.ts b/packages/specs/schema/src/decorators/common/allow.spec.ts
index fb0be426a84..7ae328976a9 100644
--- a/packages/specs/schema/src/decorators/common/allow.spec.ts
+++ b/packages/specs/schema/src/decorators/common/allow.spec.ts
@@ -68,8 +68,15 @@ describe("@Allow", () => {
expect(classSchema).toEqual({
properties: {
allow: {
- minLength: 1,
- type: ["null", "string"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ minLength: 1,
+ type: "string"
+ }
+ ]
}
},
required: ["allow"],
@@ -108,7 +115,7 @@ describe("@Allow", () => {
},
properties: {
allow: {
- oneOf: [
+ anyOf: [
{
type: "null"
},
diff --git a/packages/specs/schema/src/decorators/common/allow.ts b/packages/specs/schema/src/decorators/common/allow.ts
index 600444eeddb..8d4ef95319e 100644
--- a/packages/specs/schema/src/decorators/common/allow.ts
+++ b/packages/specs/schema/src/decorators/common/allow.ts
@@ -35,8 +35,6 @@ export function Allow(...values: any[]) {
return useDecorators(
model && Property(model),
JsonEntityFn((store, args) => {
- store.schema.allow(...values);
-
if (store.decoratorType === DecoratorTypes.PARAM) {
(store as JsonParameterStore).required = true;
}
@@ -44,6 +42,8 @@ export function Allow(...values: any[]) {
if (store.decoratorType === DecoratorTypes.PROP) {
store.parentSchema.addRequired(store.propertyName);
}
+
+ store.schema.allow(...values);
})
);
}
diff --git a/packages/specs/schema/src/decorators/common/any.spec.ts b/packages/specs/schema/src/decorators/common/any.spec.ts
index d2efb87f73d..1d86c333fc6 100644
--- a/packages/specs/schema/src/decorators/common/any.spec.ts
+++ b/packages/specs/schema/src/decorators/common/any.spec.ts
@@ -13,7 +13,30 @@ describe("@Any", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
prop: {
- type: ["null", "integer", "number", "string", "boolean", "array", "object"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ multipleOf: 1,
+ type: "integer"
+ },
+ {
+ type: "number"
+ },
+ {
+ type: "string"
+ },
+ {
+ type: "boolean"
+ },
+ {
+ type: "array"
+ },
+ {
+ type: "object"
+ }
+ ]
}
},
type: "object"
@@ -47,7 +70,20 @@ describe("@Any", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
prop: {
- type: ["null", "string", "number", "boolean"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ type: "string"
+ },
+ {
+ type: "number"
+ },
+ {
+ type: "boolean"
+ }
+ ]
}
},
type: "object"
@@ -64,7 +100,14 @@ describe("@Any", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
prop: {
- type: ["null", "string"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ type: "string"
+ }
+ ]
}
},
type: "object"
diff --git a/packages/specs/schema/src/decorators/common/enum.spec.ts b/packages/specs/schema/src/decorators/common/enum.spec.ts
index 85b15fce3fd..1527c047400 100644
--- a/packages/specs/schema/src/decorators/common/enum.spec.ts
+++ b/packages/specs/schema/src/decorators/common/enum.spec.ts
@@ -1,4 +1,3 @@
-import Ajv from "ajv";
import {enums} from "../../utils/from.js";
import {getJsonSchema} from "../../utils/getJsonSchema.js";
import {Enum} from "./enum.js";
@@ -32,8 +31,16 @@ describe("@Enum", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
num: {
- enum: ["0", "1", 10],
- type: ["string", "number"]
+ anyOf: [
+ {
+ enum: ["0", "1"],
+ type: "string"
+ },
+ {
+ enum: [10],
+ type: "number"
+ }
+ ]
}
},
type: "object"
@@ -49,8 +56,47 @@ describe("@Enum", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
num: {
- enum: ["0", "1", 10, null],
- type: ["null", "string", "number"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ enum: ["0", "1"],
+ type: "string"
+ },
+ {
+ enum: [10],
+ type: "number"
+ }
+ ]
+ }
+ },
+ type: "object"
+ });
+ });
+ it("should declare prop (mixed type, object, and null)", () => {
+ // WHEN
+ class Model {
+ @Enum("0", "1", 10, {test: "test"}, null)
+ num: string | number;
+ }
+
+ expect(getJsonSchema(Model)).toEqual({
+ properties: {
+ num: {
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ enum: ["0", "1", "test"],
+ type: "string"
+ },
+ {
+ enum: [10],
+ type: "number"
+ }
+ ]
}
},
type: "object"
@@ -125,8 +171,16 @@ describe("@Enum", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
num: {
- enum: [0, "test", "test2"],
- type: ["number", "string"]
+ anyOf: [
+ {
+ enum: [0],
+ type: "number"
+ },
+ {
+ enum: ["test", "test2"],
+ type: "string"
+ }
+ ]
}
},
type: "object"
@@ -152,8 +206,16 @@ describe("@Enum", () => {
expect(getJsonSchema(Model)).toEqual({
definitions: {
SomeEnum: {
- enum: [0, "test", "test2"],
- type: ["number", "string"]
+ anyOf: [
+ {
+ enum: [0],
+ type: "number"
+ },
+ {
+ enum: ["test", "test2"],
+ type: "string"
+ }
+ ]
}
},
properties: {
@@ -184,8 +246,16 @@ describe("@Enum", () => {
expect(getJsonSchema(Model)).toEqual({
definitions: {
SomeEnum: {
- enum: [0, "test", "test2"],
- type: ["number", "string"]
+ anyOf: [
+ {
+ enum: [0],
+ type: "number"
+ },
+ {
+ enum: ["test", "test2"],
+ type: "string"
+ }
+ ]
}
},
properties: {
@@ -197,7 +267,6 @@ describe("@Enum", () => {
});
});
});
-
describe("when is a typescript enum schema without label (set enum)", () => {
it("should inline enum", () => {
enum SomeEnum {
@@ -217,8 +286,16 @@ describe("@Enum", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
num: {
- enum: [0, "test", "test2"],
- type: ["number", "string"]
+ anyOf: [
+ {
+ enum: [0],
+ type: "number"
+ },
+ {
+ enum: ["test", "test2"],
+ type: "string"
+ }
+ ]
}
},
type: "object"
diff --git a/packages/specs/schema/src/decorators/common/groups.spec.ts b/packages/specs/schema/src/decorators/common/groups.spec.ts
index 56a5f827926..ad02e311416 100644
--- a/packages/specs/schema/src/decorators/common/groups.spec.ts
+++ b/packages/specs/schema/src/decorators/common/groups.spec.ts
@@ -405,6 +405,7 @@ describe("@Groups", () => {
type: "string"
},
password: {
+ minLength: 1,
type: "string"
}
},
@@ -419,9 +420,11 @@ describe("@Groups", () => {
expect(spec2).toEqual({
properties: {
email: {
+ minLength: 1,
type: "string"
},
firstName: {
+ minLength: 1,
type: "string"
},
id: {
@@ -429,6 +432,7 @@ describe("@Groups", () => {
type: "string"
},
lastName: {
+ minLength: 1,
type: "string"
}
},
diff --git a/packages/specs/schema/src/decorators/common/nullable.spec.ts b/packages/specs/schema/src/decorators/common/nullable.spec.ts
index 1e8c4778f72..ad7b9efd0d9 100644
--- a/packages/specs/schema/src/decorators/common/nullable.spec.ts
+++ b/packages/specs/schema/src/decorators/common/nullable.spec.ts
@@ -6,6 +6,9 @@ import {getSpec} from "../../utils/getSpec.js";
import {In} from "../operations/in.js";
import {Path} from "../operations/path.js";
import {Post} from "../operations/route.js";
+import {Format} from "./format.js";
+import {MaxLength} from "./maxLength.js";
+import {Minimum} from "./minimum.js";
import {Nullable} from "./nullable.js";
import {Property} from "./property.js";
import {Required} from "./required.js";
@@ -20,16 +23,85 @@ describe("@Nullable", () => {
}
// THEN
- expect(getJsonSchema(Model)).toEqual({
+ const schema = getJsonSchema(Model);
+
+ expect(schema).toEqual({
properties: {
prop2: {
- type: ["null", "string"],
- minLength: 1
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ type: "string",
+ minLength: 1
+ }
+ ]
}
},
required: ["prop2"],
type: "object"
});
+
+ const ajv = new Ajv({strict: true});
+
+ expect(ajv.validate(schema, {prop2: null})).toBeTruthy();
+ expect(ajv.validate(schema, {prop2: "test"})).toBeTruthy();
+ expect(ajv.validate(schema, {prop2: 1})).toBeFalsy();
+ expect(ajv.validate(schema, {prop2: ""})).toBeFalsy();
+
+ @Path("/")
+ class Test {
+ @Post("/")
+ test(@BodyParams() model: Model) {}
+ }
+
+ expect(getSpec(Test, {specType: SpecTypes.OPENAPI})).toEqual({
+ components: {
+ schemas: {
+ Model: {
+ properties: {
+ prop2: {
+ minLength: 1,
+ type: "string",
+ nullable: true
+ }
+ },
+ required: ["prop2"],
+ type: "object"
+ }
+ }
+ },
+ paths: {
+ "/": {
+ post: {
+ operationId: "testTest",
+ parameters: [],
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ $ref: "#/components/schemas/Model"
+ }
+ }
+ },
+ required: false
+ },
+ responses: {
+ "200": {
+ description: "Success"
+ }
+ },
+ tags: ["Test"]
+ }
+ }
+ },
+ tags: [
+ {
+ name: "Test"
+ }
+ ]
+ });
});
it("should declare any prop (String + Required + Nullable)", () => {
// WHEN
@@ -48,12 +120,57 @@ describe("@Nullable", () => {
expect(schema).toEqual({
properties: {
prop2: {
- type: ["null", "string"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ type: "string"
+ }
+ ]
}
},
required: ["prop2"],
type: "object"
});
+
+ @Path("/")
+ class Test {
+ @Post("/")
+ test(@BodyParams() model: Model) {}
+ }
+
+ expect(getSpec(Test, {specType: SpecTypes.OPENAPI})).toEqual({
+ components: {
+ schemas: {
+ Model: {
+ properties: {
+ prop2: {
+ type: "string",
+ nullable: true
+ }
+ },
+ required: ["prop2"],
+ type: "object"
+ }
+ }
+ },
+ paths: {
+ "/": {
+ post: {
+ operationId: "testTest",
+ parameters: [],
+ requestBody: {
+ content: {"application/json": {schema: {$ref: "#/components/schemas/Model"}}},
+ required: false
+ },
+ responses: {"200": {description: "Success"}},
+ tags: ["Test"]
+ }
+ }
+ },
+ tags: [{name: "Test"}]
+ });
});
it("should declare any prop (String + Nullable)", () => {
// WHEN
@@ -66,7 +183,14 @@ describe("@Nullable", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
prop2: {
- type: ["null", "string"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ type: "string"
+ }
+ ]
}
},
type: "object"
@@ -80,20 +204,40 @@ describe("@Nullable", () => {
}
// THEN
- expect(getJsonSchema(Model)).toEqual({
+ const schema = getJsonSchema(Model);
+ expect(schema).toEqual({
properties: {
prop2: {
- type: ["null", "string", "number"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ type: "string"
+ },
+ {
+ type: "number"
+ }
+ ]
}
},
type: "object"
});
+
+ const ajv = new Ajv({strict: true});
+
+ expect(ajv.validate(schema, {prop2: null})).toBeTruthy();
+ expect(ajv.validate(schema, {prop2: "test"})).toBeTruthy();
+ expect(ajv.validate(schema, {prop2: 1})).toBeTruthy();
+ expect(ajv.validate(schema, {prop2: false})).toBeFalsy();
});
it("should declare any prop (String & Number + Required + Nullable)", () => {
// WHEN
class Model {
@Required(true, null, "")
@Nullable(String, Number)
+ @MaxLength(10)
+ @Minimum(0)
prop2: number | string | null;
}
@@ -101,7 +245,19 @@ describe("@Nullable", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
prop2: {
- type: ["null", "string", "number"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ maxLength: 10,
+ type: "string"
+ },
+ {
+ minimum: 0,
+ type: "number"
+ }
+ ]
}
},
required: ["prop2"],
@@ -112,6 +268,7 @@ describe("@Nullable", () => {
// WHEN
class Model {
@Nullable(Date)
+ @Format("date-time")
prop2: Date | null;
}
@@ -119,7 +276,15 @@ describe("@Nullable", () => {
expect(getJsonSchema(Model)).toEqual({
properties: {
prop2: {
- type: ["null", "string"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ format: "date-time",
+ type: "string"
+ }
+ ]
}
},
type: "object"
@@ -137,8 +302,71 @@ describe("@Nullable", () => {
prop2: Nested | null;
}
+ @Path("/")
+ class Test {
+ @Post("/")
+ test(@BodyParams() model: Model) {}
+ }
+
// THEN
- expect(getJsonSchema(Model)).toEqual({
+ expect(getSpec(Test)).toEqual({
+ components: {
+ schemas: {
+ Model: {
+ properties: {
+ prop2: {
+ anyOf: [
+ {
+ $ref: "#/components/schemas/Nested"
+ }
+ ],
+ nullable: true
+ }
+ },
+ type: "object"
+ },
+ Nested: {
+ properties: {
+ id: {
+ type: "string"
+ }
+ },
+ type: "object"
+ }
+ }
+ },
+ paths: {
+ "/": {
+ post: {
+ operationId: "testTest",
+ parameters: [],
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ $ref: "#/components/schemas/Model"
+ }
+ }
+ },
+ required: false
+ },
+ responses: {
+ "200": {
+ description: "Success"
+ }
+ },
+ tags: ["Test"]
+ }
+ }
+ },
+ tags: [
+ {
+ name: "Test"
+ }
+ ]
+ });
+ const schema = getJsonSchema(Model);
+ expect(schema).toEqual({
definitions: {
Nested: {
properties: {
@@ -151,7 +379,7 @@ describe("@Nullable", () => {
},
properties: {
prop2: {
- oneOf: [
+ anyOf: [
{
type: "null"
},
@@ -163,6 +391,11 @@ describe("@Nullable", () => {
},
type: "object"
});
+
+ const ajv = new Ajv({strict: true});
+
+ expect(ajv.validate(schema, {prop2: null})).toBeTruthy();
+ expect(ajv.validate(schema, {prop2: {id: "id"}})).toBeTruthy();
});
it("should declare any prop (many Models + Nullable + JsonSchema)", () => {
// WHEN
@@ -188,12 +421,7 @@ describe("@Nullable", () => {
}
const schema = getJsonSchema(Model);
- const ajv = new Ajv({strict: true});
- ajv.validate(schema, {prop2: null});
-
- expect(ajv.errors).toBe(null);
- // THEN
expect(schema).toEqual({
definitions: {
Nested1: {
@@ -221,7 +449,7 @@ describe("@Nullable", () => {
},
properties: {
prop2: {
- oneOf: [
+ anyOf: [
{
type: "null"
},
@@ -236,6 +464,12 @@ describe("@Nullable", () => {
},
type: "object"
});
+
+ const ajv = new Ajv({strict: true});
+
+ ajv.validate(schema, {prop2: null});
+
+ expect(ajv.errors).toBe(null);
});
it("should declare any prop (many Models + Nullable + OS3)", () => {
// WHEN
@@ -273,7 +507,7 @@ describe("@Nullable", () => {
Model: {
properties: {
prop2: {
- oneOf: [
+ anyOf: [
{
$ref: "#/components/schemas/Nested1"
},
@@ -340,6 +574,56 @@ describe("@Nullable", () => {
}
]
});
+
+ const schema = getJsonSchema(Model);
+
+ expect(schema).toEqual({
+ definitions: {
+ Nested1: {
+ properties: {
+ id: {
+ type: "string"
+ },
+ top: {
+ type: "string"
+ }
+ },
+ type: "object"
+ },
+ Nested2: {
+ properties: {
+ id: {
+ type: "string"
+ },
+ other: {
+ type: "string"
+ }
+ },
+ type: "object"
+ }
+ },
+ properties: {
+ prop2: {
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ $ref: "#/definitions/Nested1"
+ },
+ {
+ $ref: "#/definitions/Nested2"
+ }
+ ]
+ }
+ },
+ type: "object"
+ });
+
+ const ajv = new Ajv({strict: true});
+
+ expect(ajv.validate(schema, {prop2: null})).toBeTruthy();
+ expect(ajv.validate(schema, {prop2: {id: "id", other: "other"}})).toBeTruthy();
});
it("should declare any prop (many Models + Nullable + OS2)", () => {
// WHEN
@@ -377,7 +661,7 @@ describe("@Nullable", () => {
Model: {
properties: {
prop2: {
- oneOf: [
+ anyOf: [
{
$ref: "#/components/schemas/Nested1"
},
diff --git a/packages/specs/schema/src/decorators/common/readOnly.spec.ts b/packages/specs/schema/src/decorators/common/readOnly.spec.ts
index 2eedbb07bfa..8c5aec08a2a 100644
--- a/packages/specs/schema/src/decorators/common/readOnly.spec.ts
+++ b/packages/specs/schema/src/decorators/common/readOnly.spec.ts
@@ -66,7 +66,7 @@ describe("@ReadOnly", () => {
type: "number"
},
members: {
- oneOf: [
+ anyOf: [
{
$ref: "#/components/schemas/TestPerson"
}
diff --git a/packages/specs/schema/src/decorators/common/required.spec.ts b/packages/specs/schema/src/decorators/common/required.spec.ts
index 9f97ec38dea..000bef52465 100644
--- a/packages/specs/schema/src/decorators/common/required.spec.ts
+++ b/packages/specs/schema/src/decorators/common/required.spec.ts
@@ -1,5 +1,5 @@
import {validateModel} from "../../../test/helpers/validateModel.js";
-import {getJsonSchema} from "../../index.js";
+import {getJsonSchema, MinLength} from "../../index.js";
import Ajv from "ajv";
import {JsonEntityStore} from "../../domain/JsonEntityStore.js";
import {Required} from "./required.js";
@@ -45,6 +45,49 @@ describe("@Required", () => {
type: "object"
});
});
+ it("should declare required field on string property", () => {
+ // WHEN
+ class Model {
+ @Required()
+ prop: string;
+ }
+
+ // THEN
+ const classSchema = JsonEntityStore.from(Model);
+
+ expect(classSchema.schema.toJSON()).toEqual({
+ properties: {
+ prop: {
+ minLength: 1,
+ type: "string"
+ }
+ },
+ required: ["prop"],
+ type: "object"
+ });
+ });
+ it("should declare required field on string property (minLength 3)", () => {
+ // WHEN
+ class Model {
+ @Required()
+ @MinLength(3)
+ prop: string;
+ }
+
+ // THEN
+ const classSchema = JsonEntityStore.from(Model);
+
+ expect(classSchema.schema.toJSON()).toEqual({
+ properties: {
+ prop: {
+ minLength: 3,
+ type: "string"
+ }
+ },
+ required: ["prop"],
+ type: "object"
+ });
+ });
it("should declare required field with a model an null", () => {
// WHEN
class NestedModel {
@@ -73,7 +116,7 @@ describe("@Required", () => {
},
properties: {
allow: {
- oneOf: [
+ anyOf: [
{
type: "null"
},
diff --git a/packages/specs/schema/src/decorators/common/requiredGroups.spec.ts b/packages/specs/schema/src/decorators/common/requiredGroups.spec.ts
index 81af79f5cca..bd16eddb279 100644
--- a/packages/specs/schema/src/decorators/common/requiredGroups.spec.ts
+++ b/packages/specs/schema/src/decorators/common/requiredGroups.spec.ts
@@ -65,6 +65,7 @@ describe("@RequiredGroups", () => {
type: "string"
},
prop3: {
+ minLength: 1,
type: "string"
}
},
@@ -87,6 +88,7 @@ describe("@RequiredGroups", () => {
type: "string"
},
prop2: {
+ minLength: 1,
type: "string"
},
prop3: {
@@ -114,6 +116,7 @@ describe("@RequiredGroups", () => {
type: "string"
},
prop3: {
+ minLength: 1,
type: "string"
}
},
diff --git a/packages/specs/schema/src/decorators/operations/partial.spec.ts b/packages/specs/schema/src/decorators/operations/partial.spec.ts
index a71c44e5cd2..b92cc0eff3e 100644
--- a/packages/specs/schema/src/decorators/operations/partial.spec.ts
+++ b/packages/specs/schema/src/decorators/operations/partial.spec.ts
@@ -1,15 +1,14 @@
+import Ajv from "ajv";
import {
CollectionOf,
getJsonSchema,
getSpec,
Groups,
In,
- Name,
OperationPath,
Path,
Property,
Required,
- RequiredGroups,
Returns,
SpecTypes
} from "../../index.js";
@@ -79,6 +78,7 @@ describe("@Partial", () => {
type: "string"
},
prop3: {
+ minLength: 1,
type: "string"
},
prop4: {
@@ -157,5 +157,92 @@ describe("@Partial", () => {
]
});
});
+ it("should return a valid json-schema", () => {
+ const schema = getJsonSchema(MyModel, {});
+
+ expect(schema).toEqual({
+ definitions: {
+ ChildModel: {
+ properties: {
+ id: {
+ type: "string"
+ },
+ prop1: {
+ minLength: 1,
+ type: "string"
+ }
+ },
+ required: ["prop1"],
+ type: "object"
+ }
+ },
+ properties: {
+ id: {
+ type: "string"
+ },
+ prop3: {
+ minLength: 1,
+ type: "string"
+ },
+ prop4: {
+ items: {
+ $ref: "#/definitions/ChildModel"
+ },
+ type: "array"
+ }
+ },
+ required: ["prop3"],
+ type: "object"
+ });
+
+ const ajv = new Ajv({strict: true});
+
+ expect(ajv.validate(schema, {})).toBe(false);
+ expect(ajv.validate(schema, {prop3: "test"})).toBe(true);
+ });
+ it("should return a valid json-schema (partial)", () => {
+ const schema = getJsonSchema(MyModel, {
+ groups: ["partial"]
+ });
+
+ expect(schema).toEqual({
+ definitions: {
+ ChildModel: {
+ properties: {
+ id: {
+ type: "string"
+ },
+ prop1: {
+ minLength: 1,
+ type: "string"
+ }
+ },
+ required: ["prop1"],
+ type: "object"
+ }
+ },
+ properties: {
+ id: {
+ type: "string"
+ },
+ prop3: {
+ minLength: 1,
+ type: "string"
+ },
+ prop4: {
+ items: {
+ $ref: "#/definitions/ChildModel"
+ },
+ type: "array"
+ }
+ },
+ type: "object"
+ });
+
+ const ajv = new Ajv({strict: true});
+
+ expect(ajv.validate(schema, {})).toBe(true);
+ expect(ajv.validate(schema, {prop3: "test"})).toBe(true);
+ });
});
});
diff --git a/packages/specs/schema/src/domain/JsonSchema.spec.ts b/packages/specs/schema/src/domain/JsonSchema.spec.ts
index f0d17457a74..283bc620666 100644
--- a/packages/specs/schema/src/domain/JsonSchema.spec.ts
+++ b/packages/specs/schema/src/domain/JsonSchema.spec.ts
@@ -358,8 +358,8 @@ describe("JsonSchema", () => {
expect(schema).toEqual({
type: "object",
properties: {
- name: {type: "string", minLength: 1},
- email: {type: "string", minLength: 1},
+ name: {type: "string"},
+ email: {type: "string"},
address: {type: "string"},
telephone: {type: "string"}
},
@@ -392,7 +392,7 @@ describe("JsonSchema", () => {
const schema = JsonSchema.from({
type: "object",
properties: {
- name: {type: "string"}
+ name: {type: "string", minLength: 1}
},
required: ["name"]
}).toObject();
@@ -540,15 +540,12 @@ describe("JsonSchema", () => {
it("should create a valid jsonchema", () => {
const schema = JsonSchema.from({
type: "object",
-
properties: {
name: {type: "string"},
credit_card: {type: "number"},
billing_address: {type: "string"}
},
-
required: ["name"],
-
dependencies: {
credit_card: ["billing_address"]
}
@@ -558,7 +555,7 @@ describe("JsonSchema", () => {
type: "object",
properties: {
- name: {type: "string", minLength: 1},
+ name: {type: "string"},
credit_card: {type: "number"},
billing_address: {type: "string"}
},
@@ -1068,8 +1065,19 @@ describe("JsonSchema", () => {
}).toObject();
expect(schema).toEqual({
- enum: ["red", "amber", "green", null, 42],
- type: ["null", "string", "number"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ enum: ["red", "amber", "green"],
+ type: "string"
+ },
+ {
+ enum: [42],
+ type: "number"
+ }
+ ]
});
const validate = new Ajv({allowUnionTypes: true}).compile(schema);
@@ -1370,13 +1378,12 @@ describe("JsonSchema", () => {
const jsonSchema = schema.toObject();
expect(schema.getAliasOf("prop")).toBe("aliasProp");
- expect(schema.getTarget()).toBeUndefined();
+ expect(schema.getTarget()).toBe("object");
expect(jsonSchema).toEqual({
type: "object",
properties: {
aliasProp: {
- type: "string",
- minLength: 1
+ type: "string"
}
},
required: ["aliasProp"]
@@ -1401,8 +1408,7 @@ describe("JsonSchema", () => {
type: "object",
properties: {
prop: {
- type: "string",
- minLength: 1
+ type: "string"
}
},
required: ["prop"]
@@ -1431,7 +1437,30 @@ describe("JsonSchema", () => {
const result = JsonSchema.from({type: Object}).any().toObject();
expect(result).toEqual({
- type: ["null", "integer", "number", "string", "boolean", "array", "object"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ multipleOf: 1,
+ type: "integer"
+ },
+ {
+ type: "number"
+ },
+ {
+ type: "string"
+ },
+ {
+ type: "boolean"
+ },
+ {
+ type: "array"
+ },
+ {
+ type: "object"
+ }
+ ]
});
});
});
diff --git a/packages/specs/schema/src/domain/JsonSchema.ts b/packages/specs/schema/src/domain/JsonSchema.ts
index a10b7decd07..bdda6b3f148 100644
--- a/packages/specs/schema/src/domain/JsonSchema.ts
+++ b/packages/specs/schema/src/domain/JsonSchema.ts
@@ -17,6 +17,7 @@ import {IgnoreCallback} from "../interfaces/IgnoreCallback.js";
import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions.js";
import {enumsRegistry} from "../registries/enumRegistries.js";
import {execMapper} from "../registries/JsonSchemaMapperContainer.js";
+import {string} from "../utils/from.js";
import {NestedGenerics} from "../utils/generics.js";
import {getComputedType} from "../utils/getComputedType.js";
import {getJsonType} from "../utils/getJsonType.js";
@@ -170,7 +171,7 @@ export class JsonSchema extends Map implements NestedGenerics {
}
get isNullable(): boolean {
- return this.#nullable || this.$allow.includes(null);
+ return this.#nullable;
}
get isReadOnly() {
@@ -455,7 +456,28 @@ export class JsonSchema extends Map implements NestedGenerics {
}
allow(...allow: any[]) {
+ if (([] as string[]).concat(this.getJsonType()).includes("string") && !this.has("minLength")) {
+ this.minLength(1);
+ }
+
+ allow.forEach((value) => {
+ switch (value) {
+ case "":
+ this.set("minLength", undefined);
+ break;
+ case null:
+ this.any(
+ ...["null"].concat(
+ this.getJsonType(),
+ this.$allow.map((v) => typeof v)
+ )
+ );
+ break;
+ }
+ });
+
this.$allow.push(...allow);
+
return this;
}
@@ -474,6 +496,11 @@ export class JsonSchema extends Map implements NestedGenerics {
} else {
const schema = this.clone();
schema.$selfRequired = required;
+
+ if (([] as string[]).concat(schema.getJsonType()).includes("string")) {
+ schema.minLength(1);
+ }
+
return schema;
}
@@ -604,7 +631,7 @@ export class JsonSchema extends Map implements NestedGenerics {
if (enumValue.getName()) {
super.set("enum", enumValue);
} else {
- super.set("enum", enumValue.get("enum")).any(...enumValue.getJsonType());
+ super.set("enum", enumValue.get("enum")).any(...enumValue.get("enum").map((value: any) => typeof value));
}
} else {
const {values, types} = serializeEnumValues([enumValue, enumValues].flat());
@@ -628,7 +655,7 @@ export class JsonSchema extends Map implements NestedGenerics {
* @see https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-6.26
*/
allOf(allOf: AnyJsonSchema[]) {
- super.set("allOf", allOf.map(mapToJsonSchema));
+ this.setManyOf("allOf", allOf);
return this;
}
@@ -637,7 +664,7 @@ export class JsonSchema extends Map implements NestedGenerics {
* @see https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-6.27
*/
anyOf(anyOf: AnyJsonSchema[]) {
- super.set("anyOf", anyOf.map(mapToJsonSchema));
+ this.setManyOf("anyOf", anyOf);
return this;
}
@@ -646,33 +673,7 @@ export class JsonSchema extends Map implements NestedGenerics {
* @see https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-6.28
*/
oneOf(oneOf: AnyJsonSchema[]) {
- let resolvedOneOf = oneOf.map(mapToJsonSchema);
-
- if (resolvedOneOf.length === 1 && !(oneOf[0] instanceof JsonSchema)) {
- if (!resolvedOneOf[0].hasDiscriminator) {
- return this.type(oneOf[0]);
- }
-
- const children = resolvedOneOf[0].discriminator().children();
-
- if (!children.length) {
- return this.type(oneOf[0]);
- }
-
- resolvedOneOf = children.map(mapToJsonSchema);
- }
-
- super.set("oneOf", resolvedOneOf);
-
- const jsonSchema: JsonSchema = resolvedOneOf[0];
-
- if (jsonSchema.isDiscriminator) {
- const discriminator = jsonSchema.discriminatorAncestor.discriminator();
- const {propertyName} = discriminator;
- super.set("discriminator", {propertyName});
- this.isDiscriminator = true;
- this.#discriminator = discriminator;
- }
+ this.setManyOf("oneOf", oneOf);
return this;
}
@@ -789,6 +790,7 @@ export class JsonSchema extends Map implements NestedGenerics {
*/
type(type: any | JSONSchema6TypeName | JSONSchema6TypeName[]): this {
switch (type) {
+ case "map":
case Map:
super.set("type", getJsonType(type));
this.#target = type;
@@ -798,6 +800,7 @@ export class JsonSchema extends Map implements NestedGenerics {
}
break;
+ case "array":
case Array:
super.set("type", getJsonType(type));
this.#target = type;
@@ -808,6 +811,7 @@ export class JsonSchema extends Map implements NestedGenerics {
}
break;
+ case "set":
case Set:
super.set("type", getJsonType(type));
this.#target = type;
@@ -824,6 +828,10 @@ export class JsonSchema extends Map implements NestedGenerics {
this.integer();
break;
+ case "number":
+ case "string":
+ case "boolean":
+ case "object":
case Object:
case Date:
case Boolean:
@@ -859,32 +867,16 @@ export class JsonSchema extends Map implements NestedGenerics {
}
any(...types: any[]) {
- const hasClasses = types.filter((type) => isClass(type));
-
- if (hasClasses.length >= 2) {
- this.oneOf(
- types.filter((value) => {
- if (value !== null) {
- this.nullable(true);
- return true;
- }
- return false;
- })
- );
- } else {
- if (types.length) {
- types = uniq(types).map(getJsonType);
+ types = types.length ? types : ["integer", "number", "string", "boolean", "array", "object", "null"];
- if (types.includes("null")) {
- this.nullable(true);
- types = types.filter((o) => o !== "null");
- }
- } else {
- types = ["integer", "number", "string", "boolean", "array", "object"];
- this.nullable(true);
- }
+ types = uniq(types).map((o) => {
+ return isClass(o) ? o : {type: getJsonType(o)};
+ });
- this.type(types.length === 1 ? types[0] : types);
+ if (types.length > 1) {
+ this.anyOf(types);
+ } else {
+ this.type(types[0]?.type || types[0]);
}
return this;
@@ -951,8 +943,8 @@ export class JsonSchema extends Map implements NestedGenerics {
this.#discriminator = this.#discriminator ? new Discriminator(this.#discriminator) : null;
this.isDiscriminator = obj.isDiscriminator;
this.isDiscriminatorKey = obj.isDiscriminatorKey;
-
this.#ref = obj.#ref;
+
this.#alias = new Map(this.#alias.entries());
obj.#genericLabels && (this.#genericLabels = [...obj.#genericLabels]);
this.#nestedGenerics = obj.#nestedGenerics.map((item) => [...item]);
@@ -960,6 +952,7 @@ export class JsonSchema extends Map implements NestedGenerics {
this.#isGeneric = obj.#isGeneric;
this.#isCollection = obj.#isCollection;
this.#ref = obj.#ref;
+ this.#nullable = obj.#nullable;
super.set("type", obj.get("type"));
}
@@ -994,6 +987,12 @@ export class JsonSchema extends Map implements NestedGenerics {
* Return the Json type as string
*/
getJsonType(): string | string[] {
+ if (this.get("anyOf")) {
+ return this.get("anyOf").map((o: JsonSchema) => {
+ return o.getJsonType();
+ });
+ }
+
return this.get("type") || getJsonType(this.getComputedType());
}
@@ -1011,4 +1010,42 @@ export class JsonSchema extends Map implements NestedGenerics {
clone() {
return new JsonSchema(this);
}
+
+ protected setManyOf(keyword: "oneOf" | "anyOf" | "allOf", value: AnyJsonSchema[]) {
+ let resolved = value
+ .filter((o) => {
+ if (o?.type === "null") {
+ this.nullable(true);
+ return false;
+ }
+ return true;
+ })
+ .map(mapToJsonSchema);
+
+ if (resolved.length === 1 && !(value[0] instanceof JsonSchema) && !this.isNullable) {
+ if (!resolved[0].hasDiscriminator) {
+ return this.type(value[0]);
+ }
+
+ const children = resolved[0].discriminator().children();
+
+ if (!children.length) {
+ return this.type(value[0]);
+ }
+
+ resolved = children.map(mapToJsonSchema);
+ }
+
+ super.set(keyword, resolved);
+
+ const jsonSchema: JsonSchema = resolved[0];
+
+ if (jsonSchema.isDiscriminator) {
+ const discriminator = jsonSchema.discriminatorAncestor.discriminator();
+ const {propertyName} = discriminator;
+ super.set("discriminator", {propertyName});
+ this.isDiscriminator = true;
+ this.#discriminator = discriminator;
+ }
+ }
}
diff --git a/packages/specs/schema/src/hooks/alterOneOf.ts b/packages/specs/schema/src/hooks/alterOneOf.ts
index f36e6d83886..91c8293de80 100644
--- a/packages/specs/schema/src/hooks/alterOneOf.ts
+++ b/packages/specs/schema/src/hooks/alterOneOf.ts
@@ -1,13 +1,65 @@
+import {isArray, isBoolean, isNumber, isObject, isString} from "@tsed/core";
+import type {JSONSchema6} from "json-schema";
+import {filter} from "rxjs";
+import {
+ ARRAY_PROPERTIES,
+ BOOLEAN_PROPERTIES,
+ COMMON_PROPERTIES,
+ MANY_OF_PROPERTIES,
+ NUMBER_PROPERTIES,
+ OBJECT_PROPERTIES,
+ STRING_PROPERTIES
+} from "../constants/jsonSchemaProperties.js";
import type {JsonSchema} from "../domain/JsonSchema.js";
import type {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions.js";
+function findManyOf(obj: any) {
+ return MANY_OF_PROPERTIES.find((keyword) => obj[keyword]);
+}
+
+const RULES: {[index: string]: {properties: string[]; is: (value: any) => boolean}} = {
+ string: {properties: STRING_PROPERTIES, is: isString},
+ number: {properties: NUMBER_PROPERTIES, is: isNumber},
+ boolean: {properties: BOOLEAN_PROPERTIES, is: isBoolean},
+ array: {properties: ARRAY_PROPERTIES, is: () => false},
+ object: {properties: OBJECT_PROPERTIES, is: () => false}
+};
+
+function pickProperties(type: string, obj: JSONSchema6 & Record, item: Record) {
+ const rule = RULES[type];
+
+ rule?.properties.concat(COMMON_PROPERTIES).forEach((keyword) => {
+ if (obj[keyword] !== undefined) {
+ if (COMMON_PROPERTIES.includes(keyword)) {
+ item[keyword] = (obj[keyword] as never[]).filter(rule.is) as never;
+ } else {
+ item[keyword] = obj[keyword] as never;
+ delete obj[keyword];
+ }
+ }
+ });
+}
+
export function alterOneOf(obj: any, schema: JsonSchema, options: JsonSchemaOptions) {
- if (obj.oneOf && options.groups !== false) {
- obj = {...obj, oneOf: schema.$hooks.alter("oneOf", obj.oneOf, [options.groups])};
- }
+ const kind = findManyOf(obj);
+
+ if (kind) {
+ obj[kind].forEach((item: {type: string} & Record) => {
+ pickProperties(item.type as string, obj, item);
+ });
+
+ MANY_OF_PROPERTIES.forEach((keyword) => {
+ if (obj[keyword] && options.groups !== false && schema.$hooks.has(keyword)) {
+ obj = {...obj, [keyword]: schema.$hooks.alter(keyword, obj[keyword], [options.groups])};
+ }
+ });
+
+ delete obj.const;
+ delete obj.enum;
- if ((obj.oneOf || obj.allOf || obj.anyOf) && !(obj.items || obj.properties)) {
- delete obj.type;
+ if (!(obj.items || obj.properties)) {
+ delete obj.type;
+ }
}
return obj;
diff --git a/packages/specs/schema/src/index.ts b/packages/specs/schema/src/index.ts
index 38f7f545b70..e04affaa637 100644
--- a/packages/specs/schema/src/index.ts
+++ b/packages/specs/schema/src/index.ts
@@ -29,6 +29,7 @@ export * from "./components/open-spec/operationRequestBodyMapper.js";
export * from "./components/open-spec/operationResponseMapper.js";
export * from "./components/open-spec/pathsMapper.js";
export * from "./constants/httpStatusMessages.js";
+export * from "./constants/jsonSchemaProperties.js";
export * from "./constants/OperationVerbs.js";
export * from "./decorators/class/children.js";
export * from "./decorators/class/discriminatorValue.js";
diff --git a/packages/specs/schema/src/utils/from.spec.ts b/packages/specs/schema/src/utils/from.spec.ts
index eef5c6cf6e1..4bebc922730 100644
--- a/packages/specs/schema/src/utils/from.spec.ts
+++ b/packages/specs/schema/src/utils/from.spec.ts
@@ -1,3 +1,4 @@
+import {type} from "node:os";
import {CollectionOf} from "../decorators/collections/collectionOf.js";
import {Property} from "../decorators/common/property.js";
import {
@@ -70,7 +71,30 @@ describe("from", () => {
});
expect(array().toJSON()).toEqual({type: "array"});
expect(any().toJSON()).toEqual({
- type: ["null", "integer", "number", "string", "boolean", "array", "object"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ multipleOf: 1,
+ type: "integer"
+ },
+ {
+ type: "number"
+ },
+ {
+ type: "string"
+ },
+ {
+ type: "boolean"
+ },
+ {
+ type: "array"
+ },
+ {
+ type: "object"
+ }
+ ]
});
expect(anyOf(string(), number()).toJSON()).toEqual({
diff --git a/packages/specs/schema/src/utils/getRequiredProperties.ts b/packages/specs/schema/src/utils/getRequiredProperties.ts
index 3403a7d80a0..aa4edd8dd7c 100644
--- a/packages/specs/schema/src/utils/getRequiredProperties.ts
+++ b/packages/specs/schema/src/utils/getRequiredProperties.ts
@@ -3,23 +3,6 @@ import type {JsonSchema} from "../domain/JsonSchema.js";
import {alterRequiredGroups} from "../hooks/alterRequiredGroups.js";
import type {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions.js";
-function applyStringRule(obj: any, propSchema: JsonSchema) {
- if (!propSchema?.$allow.includes("")) {
- if (([] as string[]).concat(propSchema?.get("type")).includes("string")) {
- const minLength = obj?.minLength;
- // Disallow empty string
- if (minLength === undefined) {
- return {
- ...obj,
- minLength: 1
- };
- }
- }
- }
-
- return obj;
-}
-
function mapRequiredProps(obj: any, schema: JsonSchema, options: JsonSchemaOptions = {}) {
const {useAlias} = options;
const props = Object.keys(obj.properties || {});
@@ -28,11 +11,6 @@ function mapRequiredProps(obj: any, schema: JsonSchema, options: JsonSchemaOptio
const aliasedKey = useAlias ? (schema.alias.get(key) as string) || key : key;
if (props.includes(aliasedKey)) {
- const propSchema = schema.get("properties")[key];
- const serializeSchema = obj.properties[aliasedKey];
-
- obj.properties[aliasedKey] = applyStringRule(serializeSchema, propSchema);
-
return keys.concat(aliasedKey);
}
diff --git a/packages/specs/schema/src/utils/getSpec.spec.ts b/packages/specs/schema/src/utils/getSpec.spec.ts
index b23fa32e744..b73dc772d7c 100644
--- a/packages/specs/schema/src/utils/getSpec.spec.ts
+++ b/packages/specs/schema/src/utils/getSpec.spec.ts
@@ -503,6 +503,7 @@ describe("getSpec()", () => {
name: "hello",
required: true,
schema: {
+ minLength: 1,
type: "string"
}
}
diff --git a/packages/specs/schema/src/utils/mapNullableType.ts b/packages/specs/schema/src/utils/mapNullableType.ts
index 86842fa0679..a3c1154de97 100644
--- a/packages/specs/schema/src/utils/mapNullableType.ts
+++ b/packages/specs/schema/src/utils/mapNullableType.ts
@@ -1,46 +1,68 @@
-import {uniq} from "@tsed/core";
+import {cleanObject} from "@tsed/core";
+import {MANY_OF_PROPERTIES} from "../constants/jsonSchemaProperties.js";
import type {JsonSchema} from "../domain/JsonSchema.js";
import {SpecTypes} from "../domain/SpecTypes.js";
import type {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions.js";
-function hasNullable(obj: any) {
- return obj.oneOf.find((o: any) => o.type === "null");
-}
-
export function mapNullableType(obj: any, schema: JsonSchema | null, options: JsonSchemaOptions) {
if (!schema?.isNullable) {
return obj;
}
- let types: string[] = [].concat(obj.type).filter(Boolean);
switch (options.specType) {
default:
case SpecTypes.JSON:
if (!obj.discriminator) {
- if (obj.oneOf) {
- if (!hasNullable(obj)) {
- obj.oneOf.unshift({
- type: "null"
- });
- }
+ if (obj.$ref) {
+ obj = cleanObject({
+ ...obj,
+ $ref: undefined,
+ anyOf: [
+ {type: "null"},
+ {
+ $ref: obj.$ref
+ }
+ ]
+ });
} else {
- obj.type = uniq(["null", ...types]);
+ MANY_OF_PROPERTIES.some((keyword) => {
+ if (obj[keyword]) {
+ obj[keyword] = [{type: "null"}].concat(obj[keyword].filter((item: any) => item.type !== "null"));
+ }
+ });
+ delete obj.type;
}
}
break;
case SpecTypes.OPENAPI:
- obj.nullable = true;
-
- if (!obj.oneOf) {
- if (types.length > 1) {
- obj.oneOf = types.map((type) => ({type}));
- delete obj.type;
- } else {
- obj.type = types[0];
- }
+ if (obj.$ref) {
+ return cleanObject({
+ ...obj,
+ ...(obj.$ref && {
+ anyOf: [
+ {
+ $ref: obj.$ref
+ }
+ ],
+ type: undefined
+ }),
+ nullable: true,
+ $ref: undefined
+ });
}
- break;
+ return cleanObject({
+ ...obj,
+ ...(obj.anyOf?.length === 1
+ ? {
+ ...obj.anyOf[0],
+ anyOf: undefined
+ }
+ : {
+ type: obj.anyOf?.length > 1 ? undefined : obj.type
+ }),
+ nullable: true
+ });
}
return obj;
diff --git a/packages/specs/schema/src/utils/ref.ts b/packages/specs/schema/src/utils/ref.ts
index cecfd872f70..b994e444fbe 100644
--- a/packages/specs/schema/src/utils/ref.ts
+++ b/packages/specs/schema/src/utils/ref.ts
@@ -3,6 +3,7 @@ import {pascalCase} from "change-case";
import type {JsonSchema} from "../domain/JsonSchema.js";
import {SpecTypes} from "../domain/SpecTypes.js";
import {JsonSchemaOptions} from "../interfaces/JsonSchemaOptions.js";
+import {anyOf} from "./from.js";
/**
* ignore
@@ -35,29 +36,18 @@ export function createRef(name: string, schema: JsonSchema, options: JsonSchemaO
$ref: `${host}/${name}`
};
- const nullable = schema.isNullable;
const readOnly = schema.isReadOnly;
const writeOnly = schema.isWriteOnly;
- if (nullable || readOnly || writeOnly) {
- switch (options.specType) {
- case SpecTypes.OPENAPI:
- return cleanObject({
- nullable: nullable ? true : undefined,
- readOnly: readOnly ? true : undefined,
- writeOnly: writeOnly ? true : undefined,
- oneOf: [ref]
- });
- case SpecTypes.JSON:
- return cleanObject({
- readOnly,
- writeOnly,
- oneOf: [ref]
- });
- }
- }
-
- return ref;
+ return cleanObject({
+ readOnly: readOnly ? true : undefined,
+ writeOnly: writeOnly ? true : undefined,
+ ...(readOnly || writeOnly
+ ? {
+ anyOf: [ref]
+ }
+ : ref)
+ });
}
/**
diff --git a/packages/specs/schema/test/integrations/__snapshots__/body-params-any.integration.spec.ts.snap b/packages/specs/schema/test/integrations/__snapshots__/body-params-any.integration.spec.ts.snap
index 823d803b9e7..4f0984e4142 100644
--- a/packages/specs/schema/test/integrations/__snapshots__/body-params-any.integration.spec.ts.snap
+++ b/packages/specs/schema/test/integrations/__snapshots__/body-params-any.integration.spec.ts.snap
@@ -178,9 +178,9 @@ Object {
"application/json": Object {
"schema": Object {
"items": Object {
- "nullable": true,
- "oneOf": Array [
+ "anyOf": Array [
Object {
+ "multipleOf": 1,
"type": "integer",
},
Object {
@@ -199,6 +199,7 @@ Object {
"type": "object",
},
],
+ "nullable": true,
},
"type": "array",
},
@@ -248,9 +249,9 @@ Object {
"content": Object {
"application/json": Object {
"schema": Object {
- "nullable": true,
- "oneOf": Array [
+ "anyOf": Array [
Object {
+ "multipleOf": 1,
"type": "integer",
},
Object {
@@ -269,6 +270,7 @@ Object {
"type": "object",
},
],
+ "nullable": true,
},
},
},
diff --git a/packages/specs/schema/test/integrations/__snapshots__/partial.integration.spec.ts.snap b/packages/specs/schema/test/integrations/__snapshots__/partial.integration.spec.ts.snap
index 7d5e03a6aac..ba30a547f7b 100644
--- a/packages/specs/schema/test/integrations/__snapshots__/partial.integration.spec.ts.snap
+++ b/packages/specs/schema/test/integrations/__snapshots__/partial.integration.spec.ts.snap
@@ -36,6 +36,7 @@ Object {
"type": "array",
},
"title": Object {
+ "minLength": 1,
"type": "string",
},
},
diff --git a/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap b/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap
index 663df245717..2e2a9426424 100644
--- a/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap
+++ b/packages/specs/schema/test/integrations/__snapshots__/petstore.integration.spec.ts.snap
@@ -370,6 +370,7 @@ Object {
},
"name": Object {
"example": "doggie",
+ "minLength": 1,
"type": "string",
},
"status": Object {
@@ -557,6 +558,7 @@ Object {
},
"name": Object {
"example": "doggie",
+ "minLength": 1,
"type": "string",
},
"status": Object {
diff --git a/packages/specs/schema/test/integrations/discriminator.integration.spec.ts b/packages/specs/schema/test/integrations/discriminator.integration.spec.ts
index c667723a481..90702a01a8d 100644
--- a/packages/specs/schema/test/integrations/discriminator.integration.spec.ts
+++ b/packages/specs/schema/test/integrations/discriminator.integration.spec.ts
@@ -1,6 +1,5 @@
import {Controller} from "@tsed/di";
import {BodyParams, PathParams} from "@tsed/platform-params";
-import * as Path from "path";
import {
DiscriminatorKey,
DiscriminatorValue,
@@ -448,6 +447,7 @@ describe("Discriminator", () => {
ActionPartial: {
properties: {
event: {
+ minLength: 1,
type: "string"
},
meta: {
@@ -467,6 +467,7 @@ describe("Discriminator", () => {
CustomActionPartial: {
properties: {
event: {
+ minLength: 1,
type: "string"
},
meta: {
@@ -494,6 +495,7 @@ describe("Discriminator", () => {
type: "string"
},
url: {
+ minLength: 1,
type: "string"
},
value: {
@@ -506,7 +508,6 @@ describe("Discriminator", () => {
discriminator: {
propertyName: "type"
},
- required: ["type"],
oneOf: [
{
$ref: "#/definitions/PageViewPartial"
@@ -517,7 +518,8 @@ describe("Discriminator", () => {
{
$ref: "#/definitions/CustomActionPartial"
}
- ]
+ ],
+ required: ["type"]
});
});
});
@@ -559,6 +561,7 @@ describe("Discriminator", () => {
ActionPartial: {
properties: {
event: {
+ minLength: 1,
type: "string"
},
meta: {
@@ -598,6 +601,7 @@ describe("Discriminator", () => {
CustomActionPartial: {
properties: {
event: {
+ minLength: 1,
type: "string"
},
meta: {
@@ -643,6 +647,7 @@ describe("Discriminator", () => {
type: "string"
},
url: {
+ minLength: 1,
type: "string"
},
value: {
@@ -666,7 +671,6 @@ describe("Discriminator", () => {
discriminator: {
propertyName: "type"
},
- nullable: true,
oneOf: [
{
$ref: "#/components/schemas/PageViewPartial"
@@ -914,6 +918,7 @@ describe("Discriminator", () => {
ActionPartial: {
properties: {
event: {
+ minLength: 1,
type: "string"
},
meta: {
@@ -953,6 +958,7 @@ describe("Discriminator", () => {
CustomActionPartial: {
properties: {
event: {
+ minLength: 1,
type: "string"
},
meta: {
@@ -998,6 +1004,7 @@ describe("Discriminator", () => {
type: "string"
},
url: {
+ minLength: 1,
type: "string"
},
value: {
diff --git a/packages/specs/schema/test/integrations/nullable.integration.spec.ts b/packages/specs/schema/test/integrations/nullable.integration.spec.ts
index 92673347a73..5c725a58c7c 100644
--- a/packages/specs/schema/test/integrations/nullable.integration.spec.ts
+++ b/packages/specs/schema/test/integrations/nullable.integration.spec.ts
@@ -47,26 +47,50 @@ describe("Spec: Nullable", () => {
}
},
properties: {
+ description: {
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ minLength: 1,
+ type: "string"
+ }
+ ]
+ },
id: {
type: "string"
},
+ nested: {
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ $ref: "#/definitions/Nested"
+ }
+ ]
+ },
price: {
- type: ["null", "number"]
+ anyOf: [
+ {
+ type: "null"
+ },
+ {
+ type: "number"
+ }
+ ]
},
priceDetails: {
- type: ["null", "string", "number"]
- },
- description: {
- minLength: 1,
- type: ["null", "string"]
- },
- nested: {
- oneOf: [
+ anyOf: [
{
type: "null"
},
{
- $ref: "#/definitions/Nested"
+ type: "string"
+ },
+ {
+ type: "number"
}
]
}
@@ -91,34 +115,34 @@ describe("Spec: Nullable", () => {
},
Product: {
properties: {
+ description: {
+ minLength: 1,
+ nullable: true,
+ type: "string"
+ },
id: {
type: "string"
},
- price: {
- type: "number",
+ nested: {
+ anyOf: [
+ {
+ $ref: "#/components/schemas/Nested"
+ }
+ ],
nullable: true
},
- priceDetails: {
+ price: {
nullable: true,
- oneOf: [
+ type: "number"
+ },
+ priceDetails: {
+ anyOf: [
{
type: "string"
},
{
type: "number"
}
- ]
- },
- description: {
- type: "string",
- minLength: 1,
- nullable: true
- },
- nested: {
- oneOf: [
- {
- $ref: "#/components/schemas/Nested"
- }
],
nullable: true
}