Skip to content

Nullable schemas #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-schema-for-openapi",
"version": "0.4.3",
"version": "0.5.0",
"description": "Converts a regular JSON Schema to a compatible OpenAPI 3.0.X Schema Object",
"keywords": [
"json",
Expand Down
39 changes: 25 additions & 14 deletions src/Convertor.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ class Convertor {
let defaultValue;
let types = schema.type;
let removeeNum = false;

const nullable = types.includes("null");
if (nullable === true) {
types = types.filter((type) => {
Expand Down Expand Up @@ -245,9 +246,13 @@ class Convertor {
oneOf.push(newTypeObj);
}

schema.oneOf = oneOf;
if (removeeNum) delete schema.enum;
delete schema.type;
if (oneOf.length > 1) {
schema.oneOf = oneOf;
delete schema.type;
if (removeeNum) delete schema.enum;
} else {
Object.assign(schema, oneOf[0]);
}
}
}

Expand Down Expand Up @@ -476,19 +481,25 @@ class Convertor {
});

if (hasNullType) {
schemaOf.forEach((obj) => {
if (obj.type !== "null") {
obj.nullable = true;
}
});
const newOf = schemaOf.filter((obj) => {
if (obj.type !== "null") return obj;
});
const nullableSchemasFiltered = schemaOf
.filter((schemaContainignNull) => {
if (schemaContainignNull.type !== "null") {
return schemaContainignNull;
}
})
.map((schemasNotContainingNull) => {
schemasNotContainingNull.nullable = true;
return schemasNotContainingNull;
});

if (isOneOf) {
schema.oneOf = newOf;
if (nullableSchemasFiltered.length > 1) {
if (isOneOf) schema.oneOf = nullableSchemasFiltered;
else schema.anyOf = nullableSchemasFiltered;
} else {
schema.anyOf = newOf;
if (isOneOf) delete schema.oneOf;
else delete schema.anyOf;

Object.assign(schema, nullableSchemasFiltered[0]);
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions test/schemas/nullProperties/nullAndType.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "JSON API Schema",
"description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
"type": "object",
"properties": {
"typedProperty": {
"type": ["null", "string"]
}
}
}
21 changes: 21 additions & 0 deletions test/schemas/ofNulls/moreThanOneanyOf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "JSON API Schema",
"description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
"type": "object",
"properties": {
"payment": {
"anyOf": [
{
"type": "null"
},
{
"type": "string"
},
{
"type": "integer"
}
]
}
}
}
21 changes: 21 additions & 0 deletions test/schemas/ofNulls/moreThanOneoneOf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "JSON API Schema",
"description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
"type": "object",
"properties": {
"payment": {
"oneOf": [
{
"type": "null"
},
{
"type": "string"
},
{
"type": "boolean"
}
]
}
}
}
135 changes: 102 additions & 33 deletions test/src/Convertor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const basic = require("../schemas/basic.json");
const invalidFieldOne = require("../schemas/invalidFields/invalidField.json");
// null property type
const nullProperty = require("../schemas/nullProperties/nullProperty.json");
const nullAndTypeProperty = require("../schemas/nullProperties/nullAndType.json");
// array types
const arrayType = require("../schemas/arrayTypes/arrayType.json");
const arrayTypeWithNull = require("../schemas/arrayTypes/arrayTypeIncludingNull.json");
Expand Down Expand Up @@ -55,6 +56,8 @@ const listOfBannedSchemas = require("../schemas/SchemasThatCannotBeConverted/lis
// anyOf/oneOf Nulls
const oneOfNull = require("../schemas/ofNulls/oneOfNull.json");
const anyOfNull = require("../schemas/ofNulls/anyOfNull.json");
const moreThanoneOfNull = require("../schemas/ofNulls/moreThanOneoneOf.json");
const moreThananyOfNull = require("../schemas/ofNulls/moreThanOneanyOf.json");
// anyOf/oneOf Nulls
const allOfProperties = require("../schemas/propertiesOutsideOf/allOf.json");
const oneOfProperties = require("../schemas/propertiesOutsideOf/oneOf.json");
Expand Down Expand Up @@ -178,24 +181,24 @@ describe("Convertor", () => {
let valid = await validator.validateInner(cloned, {});
expect(valid).to.be.true;
});
});

describe("arrays of types", () => {
it("should convert properties that have an array of types to a oneOf", async function () {
const newConvertor = new Convertor(arrayType);
it(`should set types as nullable when null is provided along with a type`, async function () {
const newConvertor = new Convertor(nullAndTypeProperty);
const result = newConvertor.convert("basic");

expect(result.schemas.basic.properties.typedProperty).to.have.property(
"type"
);
expect(result.schemas.basic.properties.typedProperty).to.have.property(
"type",
"string"
);
expect(result.schemas.basic.properties.typedProperty).to.have.property(
"nullable"
);
expect(
result.schemas.basic.properties.arrayTypeProperty
).to.not.have.property("type");
expect(
result.schemas.basic.properties.arrayTypeProperty
).to.have.property("oneOf");
expect(
result.schemas.basic.properties.arrayTypeProperty.oneOf
).to.be.an("array");
expect(
result.schemas.basic.properties.arrayTypeProperty.oneOf.length
).to.be.equal(2);
result.schemas.basic.properties.typedProperty.nullable
).to.be.equal(true);

const cloned = JSON.parse(JSON.stringify(basicOpenAPI));
Object.assign(cloned, { components: result });
Expand All @@ -205,9 +208,11 @@ describe("Convertor", () => {
let valid = await validator.validateInner(cloned, {});
expect(valid).to.be.true;
});
});

it("should convert properties that have an array of types to a oneOf with null fields", async function () {
const newConvertor = new Convertor(arrayTypeWithNull);
describe("arrays of types", () => {
it("should convert properties that have an array of types to a oneOf", async function () {
const newConvertor = new Convertor(arrayType);
const result = newConvertor.convert("basic");
expect(
result.schemas.basic.properties.arrayTypeProperty
Expand All @@ -220,13 +225,7 @@ describe("Convertor", () => {
).to.be.an("array");
expect(
result.schemas.basic.properties.arrayTypeProperty.oneOf.length
).to.be.equal(1);
expect(
result.schemas.basic.properties.arrayTypeProperty.oneOf[0].type
).to.be.equal("string");
expect(
result.schemas.basic.properties.arrayTypeProperty.oneOf[0].nullable
).to.be.equal(true);
).to.be.equal(2);

const cloned = JSON.parse(JSON.stringify(basicOpenAPI));
Object.assign(cloned, { components: result });
Expand Down Expand Up @@ -715,13 +714,18 @@ describe("Convertor", () => {
it("should convert an anyOf with a type of null", async function () {
const newConvertor = new Convertor(anyOfNull);
const result = newConvertor.convert("basic");
expect(result.schemas.basic.properties.payment).to.have.property(

expect(result.schemas.basic.properties.payment).to.not.have.property(
"anyOf"
);
expect(result.schemas.basic.properties.payment.anyOf).to.be.an("array");
expect(
result.schemas.basic.properties.payment.anyOf.length
).to.be.equal(1);
expect(result.schemas.basic.properties.payment).to.have.property(
"type",
"string"
);
expect(result.schemas.basic.properties.payment).to.have.property(
"nullable",
true
);

const cloned = JSON.parse(JSON.stringify(basicOpenAPI));
Object.assign(cloned, { components: result });
Expand All @@ -735,13 +739,78 @@ describe("Convertor", () => {
it("should convert a oneOf with a type of null", async function () {
const newConvertor = new Convertor(oneOfNull);
const result = newConvertor.convert("basic");
expect(result.schemas.basic.properties.payment).to.not.have.property(
"oneOf"
);
expect(result.schemas.basic.properties.payment).to.have.property(
"type",
"string"
);
expect(result.schemas.basic.properties.payment).to.have.property(
"nullable",
true
);

const cloned = JSON.parse(JSON.stringify(basicOpenAPI));
Object.assign(cloned, { components: result });
expect(cloned).to.have.property("components");
expect(cloned.components).to.have.property("schemas");
expect(cloned.components.schemas).to.have.property("basic");
let valid = await validator.validateInner(cloned, {});
expect(valid).to.be.true;
});

it("should convert an anyOf with a type of null and more than one non null type", async function () {
const newConvertor = new Convertor(moreThananyOfNull);
const result = newConvertor.convert("basic");

expect(result.schemas.basic.properties.payment).to.have.property(
"anyOf"
);
expect(result.schemas.basic.properties.payment.anyOf).to.have.lengthOf(
2
);
const stringAnyOf =
result.schemas.basic.properties.payment.anyOf.filter(
(schema) => schema.type === "string"
);
expect(stringAnyOf[0]).to.have.property("nullable", true);

const integerAnyOf =
result.schemas.basic.properties.payment.anyOf.filter(
(schema) => schema.type === "integer"
);
expect(integerAnyOf[0]).to.have.property("nullable", true);

const cloned = JSON.parse(JSON.stringify(basicOpenAPI));
Object.assign(cloned, { components: result });
expect(cloned).to.have.property("components");
expect(cloned.components).to.have.property("schemas");
expect(cloned.components.schemas).to.have.property("basic");
let valid = await validator.validateInner(cloned, {});
expect(valid).to.be.true;
});

it("should convert a oneOf with a type of null and more than one non null type", async function () {
const newConvertor = new Convertor(moreThanoneOfNull);
const result = newConvertor.convert("basic");
expect(result.schemas.basic.properties.payment).to.have.property(
"oneOf"
);
expect(result.schemas.basic.properties.payment.oneOf).to.be.an("array");
expect(
result.schemas.basic.properties.payment.oneOf.length
).to.be.equal(1);
expect(result.schemas.basic.properties.payment.oneOf).to.have.lengthOf(
2
);
const stringOneOf =
result.schemas.basic.properties.payment.oneOf.filter(
(schema) => schema.type === "string"
);
expect(stringOneOf[0]).to.have.property("nullable", true);

const booleanOneOf =
result.schemas.basic.properties.payment.oneOf.filter(
(schema) => schema.type === "boolean"
);
expect(booleanOneOf[0]).to.have.property("nullable", true);

const cloned = JSON.parse(JSON.stringify(basicOpenAPI));
Object.assign(cloned, { components: result });
Expand Down
Loading