Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions php/src/JsonStructure/InstanceValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,15 @@ public function validateInstance(mixed $instance, ?array $schema = null, string
$backup = $this->errors;
$this->errors = [];
$this->validateInstance($instance, ['type' => $t], $path, $depth + 1);
if (count($this->errors) === 0) {
$branchErrors = $this->errors;
$this->errors = $backup;
if (count($branchErrors) === 0) {
$unionValid = true;
break;
}
foreach ($this->errors as $e) {
foreach ($branchErrors as $e) {
$unionErrors[] = (string) $e;
}
$this->errors = $backup;
}
if (!$unionValid) {
$this->addError("Instance at {$path} does not match any type in union: " . implode(', ', $unionErrors), $path);
Expand Down
29 changes: 29 additions & 0 deletions php/tests/InstanceValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,35 @@ public function testUnionTypes(): void
$this->assertGreaterThan(0, count($errors));
}

public function testUnionSuccessDoesNotWipeSiblingErrors(): void
{
$schema = [
'$id' => 'https://example.com/union-sibling.struct.json',
'name' => 'UnionSiblingErrorSchema',
'type' => 'object',
'properties' => [
'a' => ['type' => 'int64'],
'b' => ['type' => 'int64'],
'lat' => ['type' => ['double', 'null']],
],
'required' => ['a', 'b'],
];

$validator = new InstanceValidator($schema, extended: true);
$errors = $validator->validate(['a' => 1, 'b' => 2, 'lat' => 59.9]);
// a and b are numbers but int64 requires string representation
$this->assertGreaterThanOrEqual(2, count($errors));
$errorMessages = array_map(fn($e) => (string)$e, $errors);
$hasA = false;
$hasB = false;
foreach ($errorMessages as $msg) {
if (str_contains($msg, '#/a')) $hasA = true;
if (str_contains($msg, '#/b')) $hasB = true;
}
$this->assertTrue($hasA, 'Expected error for property a');
$this->assertTrue($hasB, 'Expected error for property b');
}

public function testRefInType(): void
{
$schema = [
Expand Down
7 changes: 4 additions & 3 deletions python/src/json_structure/instance_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,13 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
backup = list(self.errors)
self.errors = []
self.validate_instance(instance, {"type": t}, path)
if not self.errors:
branch_errors = self.errors
self.errors = backup
if not branch_errors:
union_valid = True
break
else:
union_errors.extend(self.errors)
self.errors = backup
union_errors.extend(branch_errors)
if not union_valid:
self.errors.append(f"Instance at {path} does not match any type in union: {union_errors}")
return self.errors
Expand Down
22 changes: 22 additions & 0 deletions python/tests/test_instance_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,28 @@ def test_union_invalid():
errors = validator.validate_instance(True)
assert any("does not match any type in union" in err for err in errors)


def test_union_success_does_not_wipe_sibling_errors():
"""Regression: a successful union branch must not discard errors from sibling properties."""
schema = {
"$schema": "https://json-structure.org/meta/extended/v0/#",
"$id": "https://test.example.com/schema/unionSiblingErrors",
"name": "unionSiblingErrorSchema",
"type": "object",
"properties": {
"a": {"type": "int64"},
"b": {"type": "int64"},
"lat": {"type": ["double", "null"]}
},
"required": ["a", "b"]
}
validator = JSONStructureInstanceValidator(schema, extended=True)
errors = validator.validate_instance({"a": 1, "b": 2, "lat": 59.9})
# a and b are numbers but int64 requires string representation
assert len(errors) >= 2
assert any("#/a" in err for err in errors)
assert any("#/b" in err for err in errors)

# -------------------------------------------------------------------
# const and enum Tests
# -------------------------------------------------------------------
Expand Down
Loading