Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ef8f772
test(json-schema): add tests for bugs, missing conversions, dependenc…
AbdelrahmanHafez Apr 13, 2026
96ab89c
fix(json-schema): fix bugs, add missing conversions, dependencies, an…
AbdelrahmanHafez Apr 13, 2026
acccd2e
test(json-schema): add regression coverage for schema parity re #3108
AbdelrahmanHafez Apr 13, 2026
5458d7d
fix(json-schema): improve conversion parity re #3108
AbdelrahmanHafez Apr 13, 2026
ea36b56
test(json-schema): add coverage for full Unicode domain support and s…
AbdelrahmanHafez Apr 13, 2026
03f869b
fix(json-schema): support full Unicode (astral planes), fix stripUnkn…
AbdelrahmanHafez Apr 13, 2026
3e0bf81
test(json-schema): add coverage for meta edge cases, dependency mergi…
AbdelrahmanHafez Apr 13, 2026
9435aa5
fix(json-schema): add coverage exclusions for unreachable defensive c…
AbdelrahmanHafez Apr 13, 2026
b1f6b79
test(json-schema): cover composite and unicode domain edge cases re #…
AbdelrahmanHafez Apr 13, 2026
6ff8574
fix(json-schema): remove coverage ignores and tighten domain parity r…
AbdelrahmanHafez Apr 13, 2026
9310506
test(json-schema): cover forbidden presence and null invalid parity r…
AbdelrahmanHafez Apr 14, 2026
3b29577
fix(json-schema): preserve forbidden presence and null invalid parity…
AbdelrahmanHafez Apr 14, 2026
f61465d
test(json-schema): cover custom extension base types re #3108
AbdelrahmanHafez Apr 14, 2026
c5fc0f7
fix(json-schema): preserve base types for custom extensions re #3108
AbdelrahmanHafez Apr 14, 2026
6ee60b2
test(json-schema): cover ordered tuple length parity re #3108
AbdelrahmanHafez Apr 15, 2026
d230112
fix(json-schema): derive ordered minItems from required items re #3108
AbdelrahmanHafez Apr 15, 2026
4e973b3
test(json-schema): cover array contains and unique parity re #3108
AbdelrahmanHafez Apr 15, 2026
614d8d6
fix(json-schema): skip lossy array contains and unique output re #3108
AbdelrahmanHafez Apr 15, 2026
dcea796
test(json-schema): cover invalid null schema branches re #3108
AbdelrahmanHafez Apr 15, 2026
9a164d6
test(json-schema): cover exclusive valid parity
AbdelrahmanHafez Apr 17, 2026
81061ac
fix(json-schema): preserve exclusive valid semantics
AbdelrahmanHafez Apr 17, 2026
2ad254e
test(json-schema): cover allow exceptions and string patterns
AbdelrahmanHafez Apr 17, 2026
ad4092a
fix(json-schema): preserve allow exceptions and pattern validators
AbdelrahmanHafez Apr 17, 2026
5005864
test(json-schema): cover date parity edge cases
AbdelrahmanHafez Apr 18, 2026
7bf5701
fix(json-schema): preserve allowed values and date annotations
AbdelrahmanHafez Apr 18, 2026
b1a1cc1
test(json-schema): drop binary format expectation
AbdelrahmanHafez Apr 18, 2026
00cd977
fix(json-schema): remove binary format from draft output
AbdelrahmanHafez Apr 18, 2026
6a8d64f
test(json-schema): cover binary contentEncoding mapping
AbdelrahmanHafez Apr 18, 2026
b09db0f
fix(json-schema): normalize binary contentEncoding values
AbdelrahmanHafez Apr 18, 2026
9ec80d0
test(json-schema): exercise standard format validation
AbdelrahmanHafez Apr 18, 2026
d9debf4
test(json-schema): validate standard formats with ajv-formats
AbdelrahmanHafez Apr 18, 2026
c101a44
test(json-schema): cover hoisted when conditionals
AbdelrahmanHafez Apr 19, 2026
70e8c41
fix(json-schema): hoist simple when conditionals
AbdelrahmanHafez Apr 19, 2026
4da6c30
test(json-schema): cover literal switch hoisting
AbdelrahmanHafez Apr 22, 2026
66fd818
fix(json-schema): hoist literal switch conditionals
AbdelrahmanHafez Apr 22, 2026
a0cfbb4
refactor(json-schema): extract conversion modules
AbdelrahmanHafez Apr 22, 2026
d5ef17b
refactor(json-schema): extract type-specific helpers
AbdelrahmanHafez Apr 22, 2026
f4b06aa
refactor(json-schema): restore extracted comments
AbdelrahmanHafez Apr 22, 2026
077f1ba
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) {
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