diff --git a/php/src/JsonStructure/InstanceValidator.php b/php/src/JsonStructure/InstanceValidator.php index ffcecd0..5533919 100644 --- a/php/src/JsonStructure/InstanceValidator.php +++ b/php/src/JsonStructure/InstanceValidator.php @@ -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); diff --git a/php/tests/InstanceValidatorTest.php b/php/tests/InstanceValidatorTest.php index a0686e8..5c0dc68 100644 --- a/php/tests/InstanceValidatorTest.php +++ b/php/tests/InstanceValidatorTest.php @@ -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 = [ diff --git a/python/src/json_structure/instance_validator.py b/python/src/json_structure/instance_validator.py index c284cca..84503a6 100644 --- a/python/src/json_structure/instance_validator.py +++ b/python/src/json_structure/instance_validator.py @@ -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 diff --git a/python/tests/test_instance_validator.py b/python/tests/test_instance_validator.py index 842ee8a..fec9f5f 100644 --- a/python/tests/test_instance_validator.py +++ b/python/tests/test_instance_validator.py @@ -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 # -------------------------------------------------------------------