Skip to content

Commit a6788e3

Browse files
committed
Add OpenAPI 3.1 support: null type, type arrays, and optional array items
- Add support for type: 'null' as a standalone type - Add support for type arrays (e.g. type: ['string', 'null']) by converting to anyOf - Make array schema items optional to handle arrays without item constraints - Add OpenAPINullSchema and OpenAPITypeArraySchema types
1 parent 46fb2a5 commit a6788e3

5 files changed

Lines changed: 425 additions & 3 deletions

File tree

src/compileValueSchema.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
OpenAPIOneOfSchema,
1515
OpenAPIPropertyNamesSchema,
1616
OpenAPIStringSchema,
17+
OpenAPITypeArraySchema,
1718
OpenAPIValueSchema,
1819
} from './types';
1920
import { ValidationErrorIdentifier } from './error';
@@ -52,6 +53,11 @@ export function compileValueSchema(compiler: Compiler, schema: OpenAPIValueSchem
5253
}
5354

5455
if ('type' in schema) {
56+
// OpenAPI 3.1: type can be an array, e.g. ["string", "null"]
57+
if (Array.isArray(schema.type)) {
58+
return compileTypeArraySchema(compiler, schema as OpenAPITypeArraySchema);
59+
}
60+
5561
switch (schema.type) {
5662
case 'object':
5763
return compileObjectSchema(compiler, schema);
@@ -64,6 +70,8 @@ export function compileValueSchema(compiler: Compiler, schema: OpenAPIValueSchem
6470
return compileBooleanSchema(compiler, schema);
6571
case 'array':
6672
return compileArraySchema(compiler, schema);
73+
case 'null':
74+
return compileNullTypeSchema(compiler, schema);
6775
default:
6876
throw new Error(`Unsupported schema: ${JSON.stringify(schema)}`);
6977
}
@@ -83,6 +91,42 @@ function normalizePropertyNamesSchema(schema: OpenAPIPropertyNamesSchema): OpenA
8391
};
8492
}
8593

94+
/**
95+
* OpenAPI 3.1: type: 'null' as a standalone type.
96+
*/
97+
function compileNullTypeSchema(compiler: Compiler, schema: object) {
98+
return compiler.declareValidationFunction(schema, ({ value, error }) => {
99+
return [
100+
builders.ifStatement(
101+
builders.binaryExpression('!==', value, builders.literal(null)),
102+
builders.blockStatement([builders.returnStatement(error('expected null'))]),
103+
),
104+
builders.returnStatement(value),
105+
];
106+
});
107+
}
108+
109+
/**
110+
* OpenAPI 3.1: type as array, e.g. type: ['string', 'null']
111+
* Converts to an anyOf schema internally.
112+
*/
113+
function compileTypeArraySchema(compiler: Compiler, schema: OpenAPITypeArraySchema) {
114+
const { type, ...rest } = schema;
115+
116+
const typesWithoutNull = type.filter((t) => t !== 'null');
117+
const hasNull = type.includes('null');
118+
119+
const anyOf: OpenAPIValueSchema[] = typesWithoutNull.map(
120+
(t) => ({ ...rest, type: t }) as OpenAPIValueSchema,
121+
);
122+
123+
if (hasNull) {
124+
anyOf.push({ type: 'null' } as OpenAPIValueSchema);
125+
}
126+
127+
return compileAnyOfSchema(compiler, { anyOf });
128+
}
129+
86130
function compileAnyOfSchema(compiler: Compiler, schema: OpenAPIAnyOfSchema) {
87131
return compiler.declareValidationFunction(schema, ({ value, path, context, error }) => {
88132
const nodes: namedTypes.BlockStatement['body'] = [];
@@ -646,6 +690,11 @@ function compileArraySchema(compiler: Compiler, schema: OpenAPIArraySchema) {
646690
);
647691
}
648692

693+
if (!schema.items) {
694+
nodes.push(builders.returnStatement(value));
695+
return nodes;
696+
}
697+
649698
const valueSet = builders.identifier('valueSet');
650699

651700
if (schema.uniqueItems) {

src/tests/__snapshots__/compileValueSchema.test.ts.snap

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,88 @@ function obj0(path, value, context) {
14921492
}"
14931493
`;
14941494
1495+
exports[`Array without items 1`] = `
1496+
"/**
1497+
Validate a request against the OpenAPI spec
1498+
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} request - Input request to validate
1499+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions
1500+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
1501+
*/
1502+
export function validateRequest(request, context) {
1503+
return new RequestError(404, 'no operation match path');
1504+
}
1505+
/**
1506+
Map of all components defined in the spec to their validation functions.
1507+
{Object.<string, <T>(path: string[], value: T, context: any) => (T | ValidationError)>}
1508+
*/
1509+
export const componentSchemas = {};
1510+
export class RequestError extends Error {
1511+
/** @param {number} code HTTP code for the error
1512+
@param {string} message The error message*/
1513+
constructor(code, message) {
1514+
super(message);
1515+
/** @type {number} HTTP code for the error*/
1516+
this.code = code;
1517+
}
1518+
}
1519+
export class ValidationError extends RequestError {
1520+
/** @param {string[]} path The path that failed validation
1521+
@param {string} message The error message*/
1522+
constructor(path, message) {
1523+
super(409, message);
1524+
/** @type {string[]} The path that failed validation*/
1525+
this.path = path;
1526+
}
1527+
}
1528+
function obj0(path, value, context) {
1529+
if (!Array.isArray(value)) {
1530+
return new ValidationError(path, 'expected an array');
1531+
}
1532+
return value;
1533+
}"
1534+
`;
1535+
1536+
exports[`Array without items and uniqueItems 1`] = `
1537+
"/**
1538+
Validate a request against the OpenAPI spec
1539+
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} request - Input request to validate
1540+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions
1541+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
1542+
*/
1543+
export function validateRequest(request, context) {
1544+
return new RequestError(404, 'no operation match path');
1545+
}
1546+
/**
1547+
Map of all components defined in the spec to their validation functions.
1548+
{Object.<string, <T>(path: string[], value: T, context: any) => (T | ValidationError)>}
1549+
*/
1550+
export const componentSchemas = {};
1551+
export class RequestError extends Error {
1552+
/** @param {number} code HTTP code for the error
1553+
@param {string} message The error message*/
1554+
constructor(code, message) {
1555+
super(message);
1556+
/** @type {number} HTTP code for the error*/
1557+
this.code = code;
1558+
}
1559+
}
1560+
export class ValidationError extends RequestError {
1561+
/** @param {string[]} path The path that failed validation
1562+
@param {string} message The error message*/
1563+
constructor(path, message) {
1564+
super(409, message);
1565+
/** @type {string[]} The path that failed validation*/
1566+
this.path = path;
1567+
}
1568+
}
1569+
function obj0(path, value, context) {
1570+
if (!Array.isArray(value)) {
1571+
return new ValidationError(path, 'expected an array');
1572+
}
1573+
return value;
1574+
}"
1575+
`;
1576+
14951577
exports[`anyOf 1`] = `
14961578
"/**
14971579
Validate a request against the OpenAPI spec
@@ -1732,3 +1814,224 @@ function obj0(path, value, context) {
17321814
return result;
17331815
}"
17341816
`;
1817+
1818+
exports[`OpenAPI 3.1 type: null 1`] = `
1819+
"/**
1820+
Validate a request against the OpenAPI spec
1821+
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} request - Input request to validate
1822+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions
1823+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
1824+
*/
1825+
export function validateRequest(request, context) {
1826+
return new RequestError(404, 'no operation match path');
1827+
}
1828+
/**
1829+
Map of all components defined in the spec to their validation functions.
1830+
{Object.<string, <T>(path: string[], value: T, context: any) => (T | ValidationError)>}
1831+
*/
1832+
export const componentSchemas = {};
1833+
export class RequestError extends Error {
1834+
/** @param {number} code HTTP code for the error
1835+
@param {string} message The error message*/
1836+
constructor(code, message) {
1837+
super(message);
1838+
/** @type {number} HTTP code for the error*/
1839+
this.code = code;
1840+
}
1841+
}
1842+
export class ValidationError extends RequestError {
1843+
/** @param {string[]} path The path that failed validation
1844+
@param {string} message The error message*/
1845+
constructor(path, message) {
1846+
super(409, message);
1847+
/** @type {string[]} The path that failed validation*/
1848+
this.path = path;
1849+
}
1850+
}
1851+
function obj0(path, value, context) {
1852+
if (value !== null) {
1853+
return new ValidationError(path, 'expected null');
1854+
}
1855+
return value;
1856+
}"
1857+
`;
1858+
1859+
exports[`OpenAPI 3.1 type array type: [string, null] 1`] = `
1860+
"/**
1861+
Validate a request against the OpenAPI spec
1862+
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} request - Input request to validate
1863+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions
1864+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
1865+
*/
1866+
export function validateRequest(request, context) {
1867+
return new RequestError(404, 'no operation match path');
1868+
}
1869+
/**
1870+
Map of all components defined in the spec to their validation functions.
1871+
{Object.<string, <T>(path: string[], value: T, context: any) => (T | ValidationError)>}
1872+
*/
1873+
export const componentSchemas = {};
1874+
export class RequestError extends Error {
1875+
/** @param {number} code HTTP code for the error
1876+
@param {string} message The error message*/
1877+
constructor(code, message) {
1878+
super(message);
1879+
/** @type {number} HTTP code for the error*/
1880+
this.code = code;
1881+
}
1882+
}
1883+
export class ValidationError extends RequestError {
1884+
/** @param {string[]} path The path that failed validation
1885+
@param {string} message The error message*/
1886+
constructor(path, message) {
1887+
super(409, message);
1888+
/** @type {string[]} The path that failed validation*/
1889+
this.path = path;
1890+
}
1891+
}
1892+
function obj1(path, value, context) {
1893+
if (typeof value !== 'string') {
1894+
return new ValidationError(path, 'expected a string');
1895+
}
1896+
return value;
1897+
}
1898+
function obj2(path, value, context) {
1899+
if (value !== null) {
1900+
return new ValidationError(path, 'expected null');
1901+
}
1902+
return value;
1903+
}
1904+
function obj0(path, value, context) {
1905+
const value0 = obj1(path, value, context);
1906+
if (!(value0 instanceof ValidationError)) {
1907+
return value0;
1908+
}
1909+
const value1 = obj2(path, value, context);
1910+
if (!(value1 instanceof ValidationError)) {
1911+
return value1;
1912+
}
1913+
return new ValidationError(path, 'expected one of the anyOf schemas to match');
1914+
}"
1915+
`;
1916+
1917+
exports[`OpenAPI 3.1 type array type: [string, number] 1`] = `
1918+
"/**
1919+
Validate a request against the OpenAPI spec
1920+
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} request - Input request to validate
1921+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions
1922+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
1923+
*/
1924+
export function validateRequest(request, context) {
1925+
return new RequestError(404, 'no operation match path');
1926+
}
1927+
/**
1928+
Map of all components defined in the spec to their validation functions.
1929+
{Object.<string, <T>(path: string[], value: T, context: any) => (T | ValidationError)>}
1930+
*/
1931+
export const componentSchemas = {};
1932+
export class RequestError extends Error {
1933+
/** @param {number} code HTTP code for the error
1934+
@param {string} message The error message*/
1935+
constructor(code, message) {
1936+
super(message);
1937+
/** @type {number} HTTP code for the error*/
1938+
this.code = code;
1939+
}
1940+
}
1941+
export class ValidationError extends RequestError {
1942+
/** @param {string[]} path The path that failed validation
1943+
@param {string} message The error message*/
1944+
constructor(path, message) {
1945+
super(409, message);
1946+
/** @type {string[]} The path that failed validation*/
1947+
this.path = path;
1948+
}
1949+
}
1950+
function obj1(path, value, context) {
1951+
if (typeof value !== 'string') {
1952+
return new ValidationError(path, 'expected a string');
1953+
}
1954+
return value;
1955+
}
1956+
function obj2(path, value, context) {
1957+
if (typeof value === 'string') {
1958+
value = Number(value);
1959+
}
1960+
if (typeof value !== 'number' || Number.isNaN(value)) {
1961+
return new ValidationError(path, 'expected a number');
1962+
}
1963+
return value;
1964+
}
1965+
function obj0(path, value, context) {
1966+
const value0 = obj1(path, value, context);
1967+
if (!(value0 instanceof ValidationError)) {
1968+
return value0;
1969+
}
1970+
const value1 = obj2(path, value, context);
1971+
if (!(value1 instanceof ValidationError)) {
1972+
return value1;
1973+
}
1974+
return new ValidationError(path, 'expected one of the anyOf schemas to match');
1975+
}"
1976+
`;
1977+
1978+
exports[`OpenAPI 3.1 type array type: [string, null] with minLength 1`] = `
1979+
"/**
1980+
Validate a request against the OpenAPI spec
1981+
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} request - Input request to validate
1982+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions
1983+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
1984+
*/
1985+
export function validateRequest(request, context) {
1986+
return new RequestError(404, 'no operation match path');
1987+
}
1988+
/**
1989+
Map of all components defined in the spec to their validation functions.
1990+
{Object.<string, <T>(path: string[], value: T, context: any) => (T | ValidationError)>}
1991+
*/
1992+
export const componentSchemas = {};
1993+
export class RequestError extends Error {
1994+
/** @param {number} code HTTP code for the error
1995+
@param {string} message The error message*/
1996+
constructor(code, message) {
1997+
super(message);
1998+
/** @type {number} HTTP code for the error*/
1999+
this.code = code;
2000+
}
2001+
}
2002+
export class ValidationError extends RequestError {
2003+
/** @param {string[]} path The path that failed validation
2004+
@param {string} message The error message*/
2005+
constructor(path, message) {
2006+
super(409, message);
2007+
/** @type {string[]} The path that failed validation*/
2008+
this.path = path;
2009+
}
2010+
}
2011+
function obj1(path, value, context) {
2012+
if (typeof value !== 'string') {
2013+
return new ValidationError(path, 'expected a string');
2014+
}
2015+
if (value.length < 1) {
2016+
return new ValidationError(path, 'expected at least 1 characters');
2017+
}
2018+
return value;
2019+
}
2020+
function obj2(path, value, context) {
2021+
if (value !== null) {
2022+
return new ValidationError(path, 'expected null');
2023+
}
2024+
return value;
2025+
}
2026+
function obj0(path, value, context) {
2027+
const value0 = obj1(path, value, context);
2028+
if (!(value0 instanceof ValidationError)) {
2029+
return value0;
2030+
}
2031+
const value1 = obj2(path, value, context);
2032+
if (!(value1 instanceof ValidationError)) {
2033+
return value1;
2034+
}
2035+
return new ValidationError(path, 'expected one of the anyOf schemas to match');
2036+
}"
2037+
`;

0 commit comments

Comments
 (0)