Skip to content

Comments

Improved json schema validation handling#7

Merged
NickVries merged 22 commits intomasterfrom
feature/MP-7662-improved-json-schema-validation-handling
Dec 3, 2025
Merged

Improved json schema validation handling#7
NickVries merged 22 commits intomasterfrom
feature/MP-7662-improved-json-schema-validation-handling

Conversation

@daniel-myparcel
Copy link
Collaborator

@daniel-myparcel daniel-myparcel commented Nov 21, 2025

✨ Moved to a more up-to-date Json schema validation library
♻️ Refactored validation to be under its own module (Validation\Validator)

  • Doing this will allow us to validate values against a schema without requiring initializing a Form instance

Jira Ticket

@daniel-myparcel daniel-myparcel changed the title Feature/mp 7662 improved json schema validation handling Improved json schema validation handling Nov 21, 2025
@daniel-myparcel daniel-myparcel marked this pull request as ready for review November 21, 2025 15:18
@M4tini M4tini self-assigned this Nov 24, 2025
{
private array $errors;
public function __construct(
private readonly ValidationResult $validationResult,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that this variable is only used inside of the constructor itself. If that's the case, we don't need it as a class property.

Suggested change
private readonly ValidationResult $validationResult,
ValidationResult $validationResult,

Comment on lines 21 to 23
if (!$result->isValid()) {
throw new ValidationException($result);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would return the result of the validator here instead and use evaluate the result and handle accordingly in the class that uses the validator instead.

The responsibility of this class should be to validate the incoming data against a schema and then return a result. Whether to actually stop the execution of the code and throw an exception is not the concern of the Validator itself, because context is important, which the Validator class is lacking.

),
),
);
dump($this->form->toJsonSchema());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dump($this->form->toJsonSchema());

/**
* @throws FormValidationException
*/
public function test_it_validates(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests related to validation have been removed, likely because the validation logic has shifted. But still, I would expect there to be at least some tests related to validation, since the validate method still exists.

If you're struggling with testing the static method, then you might consider injecting the validator, either through a constructor or a method parameter. That way the validator object can be mocked and its failed and success flows can be tested here.

protected array $schema;
protected function setUp(): void
{
parent::setUp(); // TODO: Change the autogenerated stub
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parent::setUp(); // TODO: Change the autogenerated stub
parent::setUp();

Comment on lines 118 to 158
public function test_it_fails_to_validate_required_missing(): void
{
$this->expectException(ValidationException::class);
Validator::validate([
'name_1' => 'value',
'name_2' => false,
'name_4' => [
'name_1' => 'value',
'name_3' => 'a',
],
], $this->schema);
}

public function test_it_fails_to_validate_wrong_property_type(): void
{
$this->expectException(ValidationException::class);
Validator::validate([
'name_1' => 5,
'name_2' => 'hello',
'name_3' => 'a',
'name_4' => [
'name_1' => 'value',
'name_3' => 'a',
],
], $this->schema);
}

public function test_it_fails_to_validate_invalid_enum_value(): void
{
$this->expectException(ValidationException::class);
Validator::validate([
'name_1' => 'value',
'name_2' => true,
'name_3' => 'x',
'name_4' => [
'name_1' => 'value',
'name_2' => true,
'name_3' => 'y',
],
], $this->schema);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you decide to test different use cases where validation can fail, I expect an assertion that that specific error is what failed the validation as well. For example, the first test expects validation to fail on missing required properties. If you decide to test that specifically (which you might argue is not even the validator's responsibility, because we didn't write that logic) I expect you to also have an assertion on the thrown Exception that reflects that specific problem.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added assertions to the error values, if you think it makes more sense to drop these tests altogether I'm not all too attached to it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my previous comment I mentioned that I don't think the specific cases are the responsibility of this validator. I think a better test would be to make sure the external validator class is called and that our Validator class handles according to its return value.

Copy link
Collaborator Author

@daniel-myparcel daniel-myparcel Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right now I get you. I will make the change

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced the testing in Validator with mock so that we only assert the third party library code is called as expected. I hope that I didn't miss something in your explanation definitly got a little confused with this one 🫠

Copy link
Member

@NickVries NickVries left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, forgot to click "Request changes" with my review 🙃

@M4tini M4tini assigned NickVries and unassigned M4tini Nov 26, 2025
* @throws FormValidationException
*/
public function validate(array $values): void
public function validate(array $values, ?Validator $validator = null, ?ErrorFormatter $errorFormatter = null): void
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not gonna say I'm the biggest fan of this approach but since the $errorFormatter behaviour started requiring a lot of mocking I opted for also passing it in the validate method.

Lmk what you think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes, I'm absolutely not a fan of this haha. The caller of $form->validate() now has to pass an $errorFormatter so that the Form then can pass it to the Validator, who can potentially pass it to the FormValidationException in case validation fails. That means that 4 classes are now aware of this formatter that has logic that should really only concern one of them and I don't think it's the Exception class.

Our own Validator class is already tightly coupled with the package, so it wouldn't add any coupling to have the Validator also instantiate the ErrorFormatter. Furthermore, I would personally make the Exception as dumb as possible with as little dependencies as possible. The validator can get a getErrors method that uses the ErrorFormatter to produce the errors in the required format so that the exception can just be dumb and act as an Exception only and not contain logic to format errors. Exceptions can be thrown anywhere with whatever context is available at that time. Making one accept a dependency also tightly couples the context of the Exception to the ErrorFormatter because it needs to accept the expected data to pass into the ErrorFormatter in order to produce a result. That's way too tightly coupled for my taste.

Copy link
Member

@NickVries NickVries left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, forgot to click "Request changes" with my review 🙃

* @throws FormValidationException
*/
public function validate(array $values): void
public function validate(array $values, ?Validator $validator = null, ?ErrorFormatter $errorFormatter = null): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes, I'm absolutely not a fan of this haha. The caller of $form->validate() now has to pass an $errorFormatter so that the Form then can pass it to the Validator, who can potentially pass it to the FormValidationException in case validation fails. That means that 4 classes are now aware of this formatter that has logic that should really only concern one of them and I don't think it's the Exception class.

Our own Validator class is already tightly coupled with the package, so it wouldn't add any coupling to have the Validator also instantiate the ErrorFormatter. Furthermore, I would personally make the Exception as dumb as possible with as little dependencies as possible. The validator can get a getErrors method that uses the ErrorFormatter to produce the errors in the required format so that the exception can just be dumb and act as an Exception only and not contain logic to format errors. Exceptions can be thrown anywhere with whatever context is available at that time. Making one accept a dependency also tightly couples the context of the Exception to the ErrorFormatter because it needs to accept the expected data to pass into the ErrorFormatter in order to produce a result. That's way too tightly coupled for my taste.

@daniel-myparcel daniel-myparcel force-pushed the feature/MP-7662-improved-json-schema-validation-handling branch from 0e72386 to d19b539 Compare December 3, 2025 09:25
* @throws FormValidationException
*/
public function validate(array $values): void
public function validate(array $values, ?Validator $validator = null): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I really like this entire method and how it reads now! Good job.

: [];
}

private function getValidationResult(): ValidationResult
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is called multiple times, but relies on the read-only class properties, and will therefore not yield different results. Let's cache the result to remove this duplicated step. We can cache the result on the class after the first time it's performed and check if that property is set in each call (and set it otherwise of course).

Copy link
Collaborator Author

@daniel-myparcel daniel-myparcel Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

I took a slightly different approach: init an instance of ValidationResult in the constructor and then use it to get both the isValid and getErrors. Since it never changes this will work exactly the same 👍

public function test_it_validates(): void
public function test_it_validates_success(): void
{
$this->expectNotToPerformAssertions();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super important, but maybe there is a expectNotToThrowExceptions or similar? Since that is the only thing the method does apart from invoking other classes. Besides, the mocked expectations are assertions, so I don't think we need this statement at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be such an assertion. Copilot seems to agree:

If you want to explicitly check that no exception is thrown, simply run the code without setting any exception expectations. If an exception is thrown, the test will fail automatically.

self::assertTrue($validator->isValid());
self::assertEquals([], $validator->getErrors());

$values = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put this in a separate test, so that we isolate one type of test per method and dont have to figure out what part of this test fails in case it does.

@NickVries NickVries merged commit 451c42e into master Dec 3, 2025
1 check passed
@NickVries NickVries deleted the feature/MP-7662-improved-json-schema-validation-handling branch December 3, 2025 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants