Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
120aeb0
test(json-schema): add tests for bugs, missing conversions, dependenc…
AbdelrahmanHafez Apr 13, 2026
23bfba6
fix(json-schema): fix bugs, add missing conversions, dependencies, an…
AbdelrahmanHafez Apr 13, 2026
117a67d
test(json-schema): add regression coverage for schema parity re #3108
AbdelrahmanHafez Apr 13, 2026
8235a87
fix(json-schema): improve conversion parity re #3108
AbdelrahmanHafez Apr 13, 2026
10d8229
test(json-schema): add coverage for full Unicode domain support and s…
AbdelrahmanHafez Apr 13, 2026
8ef2a66
fix(json-schema): support full Unicode (astral planes), fix stripUnkn…
AbdelrahmanHafez Apr 13, 2026
12206de
test(json-schema): add coverage for meta edge cases, dependency mergi…
AbdelrahmanHafez Apr 13, 2026
c11ba46
fix(json-schema): add coverage exclusions for unreachable defensive c…
AbdelrahmanHafez Apr 13, 2026
e21d37a
test(json-schema): cover composite and unicode domain edge cases re #…
AbdelrahmanHafez Apr 13, 2026
8097119
fix(json-schema): remove coverage ignores and tighten domain parity r…
AbdelrahmanHafez Apr 13, 2026
88be019
test(json-schema): cover forbidden presence and null invalid parity r…
AbdelrahmanHafez Apr 14, 2026
687fd27
fix(json-schema): preserve forbidden presence and null invalid parity…
AbdelrahmanHafez Apr 14, 2026
61748ec
test(json-schema): cover custom extension base types re #3108
AbdelrahmanHafez Apr 14, 2026
84c9432
fix(json-schema): preserve base types for custom extensions re #3108
AbdelrahmanHafez Apr 14, 2026
b7b60c6
test(json-schema): cover ordered tuple length parity re #3108
AbdelrahmanHafez Apr 15, 2026
b117ed5
test(json-schema): cover invalid null schema branches re #3108
AbdelrahmanHafez Apr 15, 2026
1b271fa
test(json-schema): cover exclusive valid parity
AbdelrahmanHafez Apr 17, 2026
8ec10da
fix(json-schema): preserve exclusive valid semantics
AbdelrahmanHafez Apr 17, 2026
11d98e7
test(json-schema): cover allow exceptions and string patterns
AbdelrahmanHafez Apr 17, 2026
7105478
fix(json-schema): preserve allow exceptions and pattern validators
AbdelrahmanHafez Apr 17, 2026
90ab30d
test(json-schema): cover date parity edge cases
AbdelrahmanHafez Apr 18, 2026
87aa71f
fix(json-schema): preserve allowed values and date annotations
AbdelrahmanHafez Apr 18, 2026
92e3c8c
test(json-schema): drop binary format expectation
AbdelrahmanHafez Apr 18, 2026
0101241
fix(json-schema): remove binary format from draft output
AbdelrahmanHafez Apr 18, 2026
8f41200
test(json-schema): cover binary contentEncoding mapping
AbdelrahmanHafez Apr 18, 2026
f920279
fix(json-schema): normalize binary contentEncoding values
AbdelrahmanHafez Apr 18, 2026
6a790cc
test(json-schema): exercise standard format validation
AbdelrahmanHafez Apr 18, 2026
f1f2347
test(json-schema): validate standard formats with ajv-formats
AbdelrahmanHafez Apr 18, 2026
43ac995
test(json-schema): cover hoisted when conditionals
AbdelrahmanHafez Apr 19, 2026
775b104
fix(json-schema): hoist simple when conditionals
AbdelrahmanHafez Apr 19, 2026
5787a8a
test(json-schema): cover literal switch hoisting
AbdelrahmanHafez Apr 22, 2026
705abca
fix(json-schema): hoist literal switch conditionals
AbdelrahmanHafez Apr 22, 2026
3c530b0
refactor(json-schema): extract conversion modules
AbdelrahmanHafez Apr 22, 2026
e4860c8
refactor(json-schema): extract type-specific helpers
AbdelrahmanHafez Apr 22, 2026
8c8d9f2
refactor(json-schema): restore extracted comments
AbdelrahmanHafez Apr 22, 2026
4e8b0a6
refactor(json-schema): preserve moved comments
AbdelrahmanHafez Apr 22, 2026
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
167 changes: 3 additions & 164 deletions lib/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,14 @@ const Extend = require('./extend');
const Manifest = require('./manifest');
const Messages = require('./messages');
const Modify = require('./modify');
const JsonSchema = require('./json-schema/base');
const Ref = require('./ref');
const Trace = require('./trace');
const Validator = require('./validator');
const Values = require('./values');


const internals = {
standardTypes: new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']),
jsonSchemaTarget: 'draft-2020-12',
primitiveTypes: new Set(['string', 'number', 'boolean']),
nullSchema: () => ({ type: 'null' })
};
const internals = {};


internals.Base = class {
Expand Down Expand Up @@ -69,164 +65,7 @@ internals.Base = class {

$_jsonSchema(mode, options = {}) {

if (options.target !== undefined &&
options.target !== internals.jsonSchemaTarget) {

throw new Error(`Unsupported JSON Schema target: ${options.target}`);
}

const rootCall = !options.$defs;
const defs = options.$defs ?? {};

let schema = {};

const isTypeAny = this.type === 'any';
const isOnly = this._flags.only;

const valids = this._valids && Array.from(this._valids._values).filter((v) => v !== null);
let typesOverlap = true;

// If 'only' is set, check if the allowed values' types overlap with the schema type

if (valids && valids.length && isOnly && !isTypeAny) {
const types = new Set(valids.map((v) => typeof v));
typesOverlap = types.has(this.type) || (this.type === 'date' && types.has('object'));
}

// Set the JSON Schema 'type' if it's a standard type and there's an overlap

if (!isTypeAny && typesOverlap && internals.standardTypes.has(this.type)) {
schema.type = this.type;
}

if (this._flags.description) {
schema.description = this._flags.description;
}

if (this._flags.default !== undefined && typeof this._flags.default !== 'function') {
schema.default = this._flags.default;
}

// Apply type-specific JSON Schema conversion

const subOptions = { ...options, $defs: defs };
if (this._definition.jsonSchema && typesOverlap) {
schema = this._definition.jsonSchema(this, schema, mode, subOptions);
}

// Apply rule-specific JSON Schema conversions

for (const rule of this._rules) {
const definition = this._definition.rules[rule.name];
if (definition.jsonSchema && typesOverlap && !rule._resolve.length) {
schema = definition.jsonSchema(rule, schema, isOnly, mode, subOptions);
}
}

// Handle shared schemas

if (this.$_terms.shared) {
for (const shared of this.$_terms.shared) {
defs[shared._flags.id] = shared.$_jsonSchema(mode, subOptions);
}
}

if (rootCall && Object.keys(defs).length) {
schema.$defs = defs;
}

// Handle allowed values (valids)

if (this._valids) {

const values = valids.filter((v) => typeof v !== 'symbol');
if (values.length) {
if (this._flags.only) {
schema.enum = values;

const list = Common.intersect(new Set(values.map((v) => typeof v)), internals.primitiveTypes);

if (list.size) {
const types = [...list];
schema.type = types.length === 1 ? types[0] : types;
}
}
else {
// If values are allowed but not exclusive, add them via 'anyOf' if they differ from the main type

const otherTypes = values.filter((v) => typeof v !== this.type || isTypeAny);
if (otherTypes.length && !(isTypeAny && !isOnly)) {
if (!schema.anyOf) {
schema = {
anyOf: [schema]
};
}

schema.anyOf.push({ enum: otherTypes });
}
}
}
}

// Handle 'null' if it's an allowed value

if (this._valids && this._valids.has(null) && !(isTypeAny && !isOnly)) {
if (this._valids.length === 1 && (isTypeAny || isOnly)) {
schema.type = 'null';
}
else if (schema.type) {
schema.type = [schema.type, 'null'];
}
else if (schema.anyOf) {
schema.anyOf.unshift(internals.nullSchema());
}
else {
schema = {
anyOf: [
internals.nullSchema(),
schema
]
};
}
}

// Handle conditionals (whens) by generating multiple possible schemas combined with 'anyOf'

if (this.$_terms.whens) {

const base = this.clone();
base.$_terms.whens = null;

const matches = [];
for (const when of this.$_terms.whens) {
const tests = when.is ? [when] : when.switch;
for (let i = 0; i < tests.length; ++i) {
const test = tests[i];
if (test.then) {
matches.push(base.concat(test.then).$_jsonSchema(mode, subOptions));
}

if (test.otherwise) {
matches.push(base.concat(test.otherwise).$_jsonSchema(mode, subOptions));
}

if (!test.then || (i === tests.length - 1 && !test.otherwise)) {
matches.push(base.$_jsonSchema(mode, subOptions));
}
}
}

const results = [];
for (const match of matches) {
if (!results.some((r) => deepEqual(r, match))) {
results.push(match);
}
}

return { anyOf: results };
}

return schema;
return JsonSchema.convert(this, mode, options);
}

// Rules
Expand Down
13 changes: 13 additions & 0 deletions lib/extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ exports.type = function (from, options) {
const prototype = clone(base);
const schema = from._assign(Object.create(prototype));
const def = Object.assign({}, options); // Shallow cloned
def.jsonSchemaType = internals.standardType(from);
delete def.base;

prototype._definition = def;
Expand Down Expand Up @@ -191,6 +192,8 @@ exports.type = function (from, options) {

// Helpers

internals.standardTypes = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);

internals.build = function (child, parent) {

if (!child ||
Expand All @@ -206,6 +209,16 @@ internals.build = function (child, parent) {
};


internals.standardType = function (schema) {

if (internals.standardTypes.has(schema.type)) {
return schema.type;
}

return schema._definition?.jsonSchemaType || null;
};


internals.coerce = function (child, parent) {

if (!child ||
Expand Down
17 changes: 17 additions & 0 deletions lib/json-schema/alternatives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const Conditions = require('./conditions');


exports.matches = function (schema, mode, options) {

const matches = [];

// Collect all alternative schemas from 'matches' term

for (const match of schema.$_terms.matches) {
matches.push(...Conditions.matchSchemas(match, mode, options));
}

return matches;
};
111 changes: 111 additions & 0 deletions lib/json-schema/array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict';


exports.emit = function (schema, res, mode, options) {

const ordered = schema.$_terms.ordered;

// Handle ordered items (tuple-like) using 'prefixItems'

if (ordered.length) {
res.prefixItems = ordered.map((item) => item.$_jsonSchema(mode, options));
}

if (schema.$_terms.items.length) {
const items = internals.itemsSchema(schema, mode, options);

// If there are ordered items, remaining items are 'unevaluatedItems'

if (ordered.length) {
res.unevaluatedItems = items;
internals.setOrderedMinItems(res, ordered);
}
else {
res.items = items;
}
}
else if (ordered.length) {
// No additional items allowed beyond the ordered ones

res.unevaluatedItems = false;
internals.setOrderedMinItems(res, ordered);
res.maxItems = ordered.length;
}

// Map 'has' rules to 'contains' in JSON Schema

const contains = [];
for (const rule of schema._rules) {
if (rule.name === 'has' &&
!internals.hasReferences(rule.args.schema)) {

contains.push(rule.args.schema.$_jsonSchema(mode, options));
}
}

if (contains.length) {
if (contains.length === 1) {
res.contains = contains[0];
}
else {
res.allOf = contains.map((item) => ({ contains: item }));
}
}

if (schema._flags.single &&
schema.$_terms.items.length) {

res = {
anyOf: [
res,
internals.itemsSchema(schema, mode, options)
]
};
}

return res;
};


const internals = {};


internals.itemsSchema = function (schema, mode, options) {

if (schema.$_terms.items.length === 1) {
return schema.$_terms.items[0].$_jsonSchema(mode, options);
}

return {
anyOf: schema.$_terms.items.map((item) => item.$_jsonSchema(mode, options))
};
};


internals.setOrderedMinItems = function (res, ordered) {

// Ordered items are optional by default; the array only needs to reach the
// last explicitly required position.
const minItems = internals.orderedMinItems(ordered);
if (minItems) {
res.minItems = minItems;
}
};


internals.orderedMinItems = function (ordered) {

for (let i = ordered.length - 1; i >= 0; --i) {
if (ordered[i]._flags.presence === 'required') {
return i + 1;
}
}

return 0;
};


internals.hasReferences = function (schema) {

return !!schema._refs.refs.length;
};
Loading