Strict Output Validator — JavaScript source
Check a JSON Schema against OpenAI structured-outputs strict mode rules — open objects, missing required keys, unsupported keywords — before the API rejects it. 100% client-side.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* Strict Output Validator — check a JSON Schema against OpenAI structured-
* outputs strict-mode rules, so it fails here instead of at the API.
* Port of src/lib/strictOutputValidator.ts
*
* Language: JavaScript (ES2022+, ES module; runs unmodified in Node 18+
* and modern browsers)
* Source: CosmoDev polyglot showcase port of the Strict Output Validator
* tool (slug: strict-output-validator), ported from
* src/lib/strictOutputValidator.ts (the canonical TypeScript
* implementation).
* Tool page: https://dev.cosmolabs.org/tools/strict-output-validator
* License: display source — part of CosmoDev's polyglot tool pages.
*
* Rules (2026 OpenAI strict mode):
* R1 root must be type "object" (validateStrictRoot)
* R2 every object node needs additionalProperties: false
* R3 every key in properties must be listed in required (no optional keys)
* R4 required must not name keys absent from properties
* R5 only the supported type values / keywords may appear
* The keyword allowlist is conservative: keywords OpenAI documents as
* unsupported are flagged so the verdict is actionable, not just binary.
*/
/**
* A single rule violation.
* @typedef {'root-not-object'|'missing-additional-properties'|'property-not-required'|'required-not-property'|'unsupported-type'|'unsupported-keyword'|'invalid-schema'} StrictRule
*/
/**
* @typedef {{path: string, rule: StrictRule, message: string}} StrictIssue
*/
/**
* @typedef {{ok: boolean, issues: StrictIssue[], counts: {objects: number, properties: number, enums: number}}} StrictReport
*/
/** Types strict mode supports. */
export const SUPPORTED_TYPES = new Set([
'object',
'array',
'string',
'number',
'integer',
'boolean',
]);
/** Keywords strict mode understands per-node. Everything else is flagged. */
export const SUPPORTED_KEYWORDS = new Set([
'type',
'description',
'title',
'properties',
'required',
'additionalProperties',
'items',
'enum',
'const',
'anyOf',
'allOf', // accepted only as single-element; checked in the walker
'$ref',
'$defs',
'definitions',
'format',
'nullable',
'default',
]);
/** True for plain objects (not null, not arrays). */
function isObj(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
/**
* Validate a schema against the strict-mode structural rules. Accepts either
* a pre-parsed schema object or a JSON string (parsed here; a parse failure
* is reported as a single `invalid-schema` issue). R1 (root must be an
* object) is NOT checked here — use {@link validateStrictRoot} for that.
* @param {unknown} input
* @returns {StrictReport}
*/
export function validateStrictSchema(input) {
const issues = [];
const counts = { objects: 0, properties: 0, enums: 0 };
let schema = input;
if (typeof input === 'string') {
try {
schema = JSON.parse(input);
} catch (e) {
return {
ok: false,
issues: [
{
path: '$',
rule: 'invalid-schema',
message: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
},
],
counts,
};
}
}
if (!isObj(schema)) {
return {
ok: false,
issues: [{ path: '$', rule: 'invalid-schema', message: 'Schema must be a JSON object.' }],
counts,
};
}
walk(schema, '$');
return { ok: issues.length === 0, issues, counts };
/** Flag every keyword strict mode does not understand. */
function unsupportedKeywords(node, path) {
for (const key of Object.keys(node)) {
if (!SUPPORTED_KEYWORDS.has(key)) {
issues.push({
path,
rule: 'unsupported-keyword',
message: `"${key}" is not supported in strict mode — remove it or express the constraint another way.`,
});
}
}
}
/** Depth-first walk emitting issues and accumulating counts. */
function walk(node, path) {
unsupportedKeywords(node, path);
const type = node.type;
// 'null' is only expressible inside a type array (the nullable form).
const isNullableForm = Array.isArray(type);
const typeList = isNullableForm ? type : typeof type === 'string' ? [type] : [];
for (const t of typeList) {
const supported =
typeof t === 'string' && (SUPPORTED_TYPES.has(t) || (isNullableForm && t === 'null'));
if (!supported) {
issues.push({
path,
rule: 'unsupported-type',
message: `type ${JSON.stringify(t)} is not supported — strict mode allows object, array, string, number, integer, boolean (null only inside a type array).`,
});
}
}
// allOf is accepted only as a single-element wrapper.
if (Array.isArray(node.allOf) && node.allOf.length !== 1) {
issues.push({
path,
rule: 'unsupported-keyword',
message: 'allOf is supported only with exactly one subschema (use anyOf for unions).',
});
}
if (node.type === 'object' || node.properties !== undefined || node.required !== undefined) {
counts.objects += 1;
if (node.additionalProperties !== false) {
issues.push({
path,
rule: 'missing-additional-properties',
message: 'Object needs "additionalProperties": false — strict mode rejects open objects.',
});
}
const props = isObj(node.properties) ? node.properties : {};
const required = Array.isArray(node.required) ? node.required : [];
counts.properties += Object.keys(props).length;
for (const key of Object.keys(props)) {
if (!required.includes(key)) {
issues.push({
path: `${path}.required`,
rule: 'property-not-required',
message: `"${key}" is defined in properties but missing from required — strict mode requires every property.`,
});
}
}
for (const key of required) {
if (typeof key === 'string' && !(key in props)) {
issues.push({
path: `${path}.required`,
rule: 'required-not-property',
message: `"${key}" is required but has no definition in properties.`,
});
}
}
for (const [key, sub] of Object.entries(props)) {
if (isObj(sub)) walk(sub, `${path}.properties.${key}`);
}
}
if (isObj(node.items)) {
walk(node.items, `${path}.items`);
}
if (Array.isArray(node.enum)) counts.enums += 1;
for (const listKey of ['anyOf', 'oneOf', 'allOf']) {
const list = node[listKey];
if (Array.isArray(list)) {
if (listKey === 'oneOf') {
issues.push({
path: `${path}.${listKey}`,
rule: 'unsupported-keyword',
message: 'oneOf is not supported — strict mode unions are expressed with anyOf.',
});
}
list.forEach((sub, i) => {
if (isObj(sub)) walk(sub, `${path}.${listKey}[${i}]`);
});
}
}
for (const defsKey of ['$defs', 'definitions']) {
const defs = node[defsKey];
if (isObj(defs)) {
for (const [name, sub] of Object.entries(defs)) {
if (isObj(sub)) walk(sub, `${path}.${defsKey}.${name}`);
}
}
}
}
}
/**
* Whole-report entry point: everything {@link validateStrictSchema} checks,
* plus R1 — the root schema must be type "object" (strict mode cannot return
* a bare scalar or array). The root issue is unshifted to the front.
* @param {unknown} input
* @returns {StrictReport}
*/
export function validateStrictRoot(input) {
const report = validateStrictSchema(input);
let schema = input;
if (typeof input === 'string') {
try {
schema = JSON.parse(input);
} catch {
return report; // invalid-schema already reported
}
}
if (isObj(schema) && schema.type !== 'object') {
report.issues.unshift({
path: '$',
rule: 'root-not-object',
message:
'The root schema must be type "object" — strict mode cannot return a bare scalar or array.',
});
report.ok = false;
}
return report;
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →