Crell / Serde

Robust Serde (serialization/deserialization) library for PHP 8.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow required property validation

repli2dev opened this issue · comments

Throw an error on deserialization when the required (not nullable) is not present in the payload.

Detailed description

For an API endpoint strict validation an error being thrown when the field is missing is necessary thing.

Context

This can help everyone to build strict and more safe & secure interfaces.

Possible implementation

N/A

Your environment

  • PHP 8.1 with strict types

Seems related to #7.

Long take with a proposal


  1. Simple DTOs
readonly class Foo {
public string $a;
}

Description: typically just mapped payload without additional methods, bypass encapsulation at all basically, or limiting to read only. Business logic typically asserted by another library.

Rationale: When using this the expectation is that when I deserialized this from any data I cannot really be sure whether the fields are populated or not... another layer of validation is needed, this can be provided by another library (i.e. symfony/validator) but due to the next case I would argue that this basic validation is still in within competency of deserialization.

Proposal: provide required: bool in Field attribute.


  1. Simple DTOs with encapsulation
class Foo {
public function __construct(private string $a) // or without constructor property promotion
}

Description: typically there is an attempt for data encapsulation so one can rely on the logical state of the DTO through its lifetime... so there are plenty of getters and setter to ensure that.

Rationale: Expectation is that if this object is deserializated the encapsulation still holds.

Proposal: automatically consider all properties from constructor without default value as required: true and check after/before object population.


  1. Objects implementing Serializable interface

Description: typically the programmer is responsible here for proper deserialization and population of all required fields.

Proposal: do nothing here


  1. Custom "named constructors"
class Foo {
private function __construct() {}
public static function fromArrayOne(array $props) { $a = new self(); /* ... */} 
public static function fromArrayTwo(string $one) { $a = new self(); /* ... */} 
}

Description: to my knowledge this happens quite a lot in the codebases all around, yet

Rationale: Expectation are made by the programmer in their private constructors, there does not seem to be way how to detect it automatically.

Proposal: do nothing here, discourage in the docs.


WDYT @Crell is this meaningful proposal?

What I've been pondering here is relying on unintialized. More specifically, any property can be uninitalized before it has a value. If we assume that an object with an uninitialized value is probably wrong (that is not always the case!), then a simple solution is to just check the object when we're done with it for uninitialized properties. (There's reflection for that.) If any are found, fail in some way. I'm not sure if an exception or a new SerdeError is better here. Then add a flag to the ClassSettings attribute to disable that check if someone wants.

That way, default values still get used as they do now, from the constructor or the property itself. It works with cases 1 and 2, skips 3 (which I agree, is the class author's problem), and ignores 4 unless there's a private __construct() that has default values, in which case they'll get used by the existing default logic. (And if you're not doing that, you're on your own.)

That wouldn't fully resolve the "null doesn't get assigned" issue, but that is going to be trickier and best handled separately.

Would that work for your use case(s)?

Also note that for any more complex validation, a post-load callback makes it trivial. As seen in:

https://github.com/Crell/mastobot/blob/master/src/AccountDef.php

TLDR:

  • I agree with outlined way of implementation
  • I could attempt to do a PL if you wish
  • The null assigning issue seems to be tackled separately in #16 IMHO.

I quite like it and it seems it would work for me and it would also prevent the future futile attempts to convey that the supposed required flag treats only uninitialized and field can still pass the required validation even being explicit/default null...

Though there is one more thing to consider: If we assume that an object with an uninitialized value is probably wrong (that is not always the case!)
The case against is that under OpenAPI spec you have fields that can be provided or left-out which is distinct from when you provide them with null (JS: undefined vs null). By accepting your assumption we would basically say: this is undesired pattern in PHP / using this library and you should always provide some default either in field definition, in attribute or in constructor default value.

For the more complex validation, yes this is a nice option to do some additional validation when I do not want to bring the whole symfony/validator... but for this particular case it is useless as there is (to my knowledge) no way how to distinquish null and undefined so isset mix both of the status.