Skip to content

Commit 3e3553c

Browse files
committed
feat(validator)!: return empty errors list from tryValidate on success
- Return [] instead of null so consumers can iterate without a null-guard - Drop ?? [] guards in schema validators that consume nested tryValidate errors - Update docs, CHANGELOG, and tests for the new tuple shape BREAKING CHANGE: tryValidate() now returns an empty errors list on success instead of null. Check $valid or $errors === [] rather than $errors === null.
1 parent cb62621 commit 3e3553c

13 files changed

Lines changed: 23 additions & 19 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Changed
8+
9+
- **BREAKING:** the errors element of the `tryValidate()` tuple is now an empty list `[]` on success (previously `null`), so consumers can iterate or count the errors without a null-guard. Code that detected success via `$errors === null` should check `$valid` (or `$errors === []`) instead; `$valid` remains the canonical success signal
10+
711
## [0.16.0] - 2026-07-06
812

913
### Added

docs/getting-started/basic-usage.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ try {
6262

6363
### `tryValidate()` - Tuple-based
6464

65-
Returns a tuple `[bool $valid, mixed $data, ?array $errors]`, where `$errors` is a flat list of
66-
`ValidationError` objects (or `null` on success):
65+
Returns a tuple `[bool $valid, mixed $data, array $errors]`, where `$errors` is a flat list of
66+
`ValidationError` objects (empty on success):
6767

6868
```php
6969
$validator = Validator::isString()->email();

docs/getting-started/core-concepts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ All validators extend `FieldValidator`, which provides:
4040
### Core Validation Methods
4141

4242
- `validate(mixed $value): mixed` - Throws exception on failure
43-
- `tryValidate(mixed $value): array` - Returns `[bool $valid, mixed $data, ?array $errors]`, where `$errors` is a list of `ValidationError` objects
43+
- `tryValidate(mixed $value): array` - Returns `[bool $valid, mixed $data, array $errors]`, where `$errors` is a list of `ValidationError` objects (empty on success)
4444

4545
### Common Configuration
4646

docs/guides/error-handling.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ try {
2626

2727
### Tuple-Based: `tryValidate()`
2828

29-
Returns a result tuple `[bool $valid, mixed $data, ?array $errors]`, where `$errors` is a flat
30-
list of `ValidationError` objects (or `null` on success):
29+
Returns a result tuple `[bool $valid, mixed $data, array $errors]`, where `$errors` is a flat
30+
list of `ValidationError` objects (empty on success):
3131

3232
```php
3333
$validator = Validator::isString()->email();

llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
- **Fluency:** Validators are mutable during configuration but used sequentially.
2424
- **Execution:**
2525
- `validate(mixed $value, string $key = '', mixed $input = null)` throws `ValidationException`.
26-
- `tryValidate(mixed $value, string $key = '', mixed $input = null)` returns tuple `[bool $valid, mixed $data, ?array $errors]`, where `$errors` is a flat list of `ValidationError` objects (or `null` on success).
26+
- `tryValidate(mixed $value, string $key = '', mixed $input = null)` returns tuple `[bool $valid, mixed $data, array $errors]`, where `$errors` is a flat list of `ValidationError` objects (empty on success, so it can be iterated without a null-guard).
2727
- **Fail-Fast:** Each validator chain stops at the first failing rule; schema validators aggregate errors across fields.
2828
- **Error Structure:** Validation errors are a flat list of `ValidationError` value objects, each with `getPath()` (dotted, `''` at root), `getCode()` (stable code, see `ValidationCode`), `getMessage()`, and `getParams()`. Access via `ValidationException::getErrors(?string $path = null)` or the `tryValidate` tuple. `getErrors()` (no arg) returns every error; `getErrors($path)` filters to a field and everything nested beneath it (`getErrors('')` returns only root-level errors; the trailing-dot match means `getErrors('name')` won't catch `name_full`). Match on codes, not message text. `satisfies()` accepts optional `(?string $code, array $params)`; params double as `{name}` message placeholders. `ValidationError` is `JsonSerializable` (`{path, code, message, params}`), so `json_encode($e->getErrors())` is API-ready; malformed UTF-8 param keys are normalized during serialization, and unencodable param values render as `"(complex value)"`.
2929
- **Null Handling:** Validators skip `null` values unless `required()` is called. `default()` fills in null as a last-resort fallback after the pipeline; `required()` enforces presence at the very end.

src/Lemmon/Validator/ArrayValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ protected function validateType(mixed $value, string $key): mixed
308308
);
309309

310310
if (!$valid) {
311-
foreach ($itemErrors ?? [] as $error) {
311+
foreach ($itemErrors as $error) {
312312
$errors[] = $error->withPathPrefix((string) $index);
313313
}
314314
continue;

src/Lemmon/Validator/AssociativeValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ protected function validateType(mixed $value, string $key): mixed
7070
);
7171

7272
if (!$valid) {
73-
foreach ($fieldErrors ?? [] as $error) {
73+
foreach ($fieldErrors as $error) {
7474
$errors[] = $error->withPathPrefix((string) $fieldKey);
7575
}
7676
continue;

src/Lemmon/Validator/FieldValidator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ public function validate(mixed $value, string $key = '', mixed $input = null): m
620620
[$valid, $data, $errors] = $this->tryValidate($value, $key, $input);
621621
if (!$valid) {
622622
throw new ValidationException(
623-
$errors ?? [new ValidationError('', ValidationCode::CUSTOM, 'Validation failed')],
623+
$errors !== [] ? $errors : [new ValidationError('', ValidationCode::CUSTOM, 'Validation failed')],
624624
);
625625
}
626626
return $data;
@@ -632,10 +632,10 @@ public function validate(mixed $value, string $key = '', mixed $input = null): m
632632
* @param mixed $value The value to validate.
633633
* @param string $key The key of the field being validated.
634634
* @param mixed|null $input The entire input payload (array or object).
635-
* @return array{bool, mixed, array<ValidationError>|null} A tuple containing:
635+
* @return array{bool, mixed, array<ValidationError>} A tuple containing:
636636
* - bool: true if validation is successful, false otherwise.
637637
* - mixed: The validated and potentially coerced value on success, or the (possibly coerced) input value on failure.
638-
* - array|null: A list of structured {@see ValidationError} objects on failure, or null on success.
638+
* - array: A list of structured {@see ValidationError} objects on failure, or an empty list on success, so consumers can iterate it unconditionally.
639639
*/
640640
public function tryValidate(mixed $value, string $key = '', mixed $input = null): array
641641
{
@@ -671,7 +671,7 @@ public function tryValidate(mixed $value, string $key = '', mixed $input = null)
671671
]);
672672
}
673673

674-
return [true, $processedValue, null];
674+
return [true, $processedValue, []];
675675
} catch (ValidationException $e) {
676676
return [false, $value, $e->getErrors()];
677677
}

src/Lemmon/Validator/ObjectValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ protected function validateType(mixed $value, string $key): mixed
7070
);
7171

7272
if (!$valid) {
73-
foreach ($fieldErrors ?? [] as $error) {
73+
foreach ($fieldErrors as $error) {
7474
$errors[] = $error->withPathPrefix((string) $fieldKey);
7575
}
7676
continue;

tests/ArrayValidatorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
[$valid, $data, $errors] = $validator->tryValidate(null);
168168
expect($valid)->toBe(true);
169169
expect($data)->toBe(null);
170-
expect($errors)->toBe(null);
170+
expect($errors)->toBe([]);
171171
});
172172

173173
it('should coerce empty string to empty array', function () {

0 commit comments

Comments
 (0)