Bug Description
XGtsRefValidator.visitInstance() only recurses into properties (for objects) and items (for arrays), but does not traverse into JSON Schema combinators (oneOf, anyOf, allOf). This means x-gts-ref constraints nested inside these combinators are silently ignored during instance validation.
Additionally, since Ajv (used internally by gts-ts) does not recognize x-gts-ref as a keyword, subschemas containing only x-gts-ref are treated as empty schemas {} by Ajv. When placed inside oneOf, both empty subschemas match any value, causing oneOf to always fail (it requires exactly 1 match out of N).
The combined effect: x-gts-ref inside oneOf/anyOf/allOf is broken at both validation layers.
Reproduction
Schema (action.v1.json)
{
"$id": "gts://gts.hai3.mfes.comm.action.v1~",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"type": {
"x-gts-ref": "/$id"
},
"target": {
"type": "string",
"oneOf": [
{ "x-gts-ref": "gts.hai3.mfes.ext.domain.v1~*" },
{ "x-gts-ref": "gts.hai3.mfes.ext.extension.v1~*" }
]
}
},
"required": ["type", "target"]
}
Instance (valid action targeting a domain)
{
"type": "gts.hai3.mfes.comm.action.v1~hai3.mfes.ext.mount_ext.v1",
"target": "gts.hai3.mfes.ext.domain.v1~hai3.screensets.layout.screen.v1"
}
Expected behavior
Validation passes — target matches the first x-gts-ref pattern (gts.hai3.mfes.ext.domain.v1~*).
Actual behavior
Validation fails with:
/target must match exactly one schema in oneOf
Ajv sees both oneOf subschemas as {} (since x-gts-ref is unknown to Ajv), both match the string value, and oneOf fails because 2 subschemas matched instead of 1.
Meanwhile, XGtsRefValidator never sees the x-gts-ref constraints at all because visitInstance() does not recurse into oneOf branches.
Root Cause
In src/x-gts-ref.ts, visitInstance() (lines 44-84) handles:
- ✅
schema.properties — recurses into object properties
- ✅
schema.items — recurses into array items
- ❌
schema.oneOf — not traversed
- ❌
schema.anyOf — not traversed
- ❌
schema.allOf — not traversed
Proposed Test
import { GtsStore, createJsonEntity } from '@globaltypesystem/gts-ts';
describe('XGtsRefValidator with oneOf', () => {
let store: GtsStore;
beforeEach(() => {
store = new GtsStore();
// Register the action schema
store.register(createJsonEntity({
"$id": "gts://gts.test.action.v1~",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"type": { "x-gts-ref": "/$id" },
"target": {
"type": "string",
"oneOf": [
{ "x-gts-ref": "gts.test.domain.v1~*" },
{ "x-gts-ref": "gts.test.extension.v1~*" }
]
}
},
"required": ["type", "target"]
}));
// Register domain schema (so domain IDs are resolvable)
store.register(createJsonEntity({
"$id": "gts://gts.test.domain.v1~",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": { "id": { "x-gts-ref": "/$id" } },
"required": ["id"]
}));
// Register extension schema
store.register(createJsonEntity({
"$id": "gts://gts.test.extension.v1~",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": { "id": { "x-gts-ref": "/$id" } },
"required": ["id"]
}));
// Register a domain instance
store.register(createJsonEntity({
"id": "gts.test.domain.v1~my.screen.domain.v1"
}));
// Register an extension instance
store.register(createJsonEntity({
"id": "gts.test.extension.v1~my.widget.v1"
}));
});
it('should validate action targeting a domain (oneOf branch 1)', () => {
store.register(createJsonEntity({
"type": "gts.test.action.v1~my.mount.action.v1",
"target": "gts.test.domain.v1~my.screen.domain.v1"
}));
const result = store.validateInstance(
"gts.test.action.v1~my.mount.action.v1"
);
expect(result.ok).toBe(true);
expect(result.valid).toBe(true);
});
it('should validate action targeting an extension (oneOf branch 2)', () => {
store.register(createJsonEntity({
"type": "gts.test.action.v1~my.notify.action.v1",
"target": "gts.test.extension.v1~my.widget.v1"
}));
const result = store.validateInstance(
"gts.test.action.v1~my.notify.action.v1"
);
expect(result.ok).toBe(true);
expect(result.valid).toBe(true);
});
it('should reject action targeting an invalid GTS ID', () => {
store.register(createJsonEntity({
"type": "gts.test.action.v1~my.bad.action.v1",
"target": "not-a-gts-id"
}));
const result = store.validateInstance(
"gts.test.action.v1~my.bad.action.v1"
);
expect(result.ok).toBe(false);
});
});
Suggested Fix
visitInstance() should recurse into combinator subschemas. Sketch:
// In visitInstance(), after handling properties and items:
// Recurse into oneOf/anyOf/allOf subschemas
for (const combinator of ['oneOf', 'anyOf', 'allOf']) {
if (Array.isArray(schema[combinator])) {
for (const subSchema of schema[combinator]) {
this.visitInstance(instance, subSchema, path, rootSchema, errors);
}
}
}
For oneOf semantics specifically, the validator should check that the instance matches exactly one x-gts-ref pattern among the subschemas (mirroring JSON Schema oneOf semantics).
Impact
This bug blocks any schema that uses x-gts-ref inside JSON Schema combinators. The workaround is to flatten x-gts-ref to the property level (e.g., using a broader wildcard pattern), but this loses the ability to express "target must be one of these specific type families."
Environment
@globaltypesystem/gts-ts (latest)
- Node.js 20+
- TypeScript 5.x
Bug Description
XGtsRefValidator.visitInstance()only recurses intoproperties(for objects) anditems(for arrays), but does not traverse into JSON Schema combinators (oneOf,anyOf,allOf). This meansx-gts-refconstraints nested inside these combinators are silently ignored during instance validation.Additionally, since Ajv (used internally by gts-ts) does not recognize
x-gts-refas a keyword, subschemas containing onlyx-gts-refare treated as empty schemas{}by Ajv. When placed insideoneOf, both empty subschemas match any value, causingoneOfto always fail (it requires exactly 1 match out of N).The combined effect:
x-gts-refinsideoneOf/anyOf/allOfis broken at both validation layers.Reproduction
Schema (
action.v1.json){ "$id": "gts://gts.hai3.mfes.comm.action.v1~", "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "type": { "x-gts-ref": "/$id" }, "target": { "type": "string", "oneOf": [ { "x-gts-ref": "gts.hai3.mfes.ext.domain.v1~*" }, { "x-gts-ref": "gts.hai3.mfes.ext.extension.v1~*" } ] } }, "required": ["type", "target"] }Instance (valid action targeting a domain)
{ "type": "gts.hai3.mfes.comm.action.v1~hai3.mfes.ext.mount_ext.v1", "target": "gts.hai3.mfes.ext.domain.v1~hai3.screensets.layout.screen.v1" }Expected behavior
Validation passes —
targetmatches the firstx-gts-refpattern (gts.hai3.mfes.ext.domain.v1~*).Actual behavior
Validation fails with:
Ajv sees both
oneOfsubschemas as{}(sincex-gts-refis unknown to Ajv), both match the string value, andoneOffails because 2 subschemas matched instead of 1.Meanwhile,
XGtsRefValidatornever sees thex-gts-refconstraints at all becausevisitInstance()does not recurse intooneOfbranches.Root Cause
In
src/x-gts-ref.ts,visitInstance()(lines 44-84) handles:schema.properties— recurses into object propertiesschema.items— recurses into array itemsschema.oneOf— not traversedschema.anyOf— not traversedschema.allOf— not traversedProposed Test
Suggested Fix
visitInstance()should recurse into combinator subschemas. Sketch:For
oneOfsemantics specifically, the validator should check that the instance matches exactly onex-gts-refpattern among the subschemas (mirroring JSON SchemaoneOfsemantics).Impact
This bug blocks any schema that uses
x-gts-refinside JSON Schema combinators. The workaround is to flattenx-gts-refto the property level (e.g., using a broader wildcard pattern), but this loses the ability to express "target must be one of these specific type families."Environment
@globaltypesystem/gts-ts(latest)