fix: Recurse into object properties and array items when coercing parameters (#60) - #63
Open
shadowhand wants to merge 1 commit into
Open
fix: Recurse into object properties and array items when coercing parameters (#60)#63shadowhand wants to merge 1 commit into
shadowhand wants to merge 1 commit into
Conversation
…ameters (duyler#60) TypeCoercer::coerceToType() had arms for integer, number, boolean and string; object and array fell through to normalizeValue(), which returns arrays untouched. AbstractParameterValidator then handed the un-coerced container to the schema validator, which correctly rejected "3" against type: integer. The effect was not merely strict: query, path, header and cookie values arrive as strings, so coercion is the only mechanism by which a nested integer, number or boolean could ever validate. Against `page: {type: object, properties: {limit: {type: integer}}}` there was no value a client could send that would pass, which a JSON:API style API hits immediately on page[limit], page[offset] and boolean filter members. Type dispatch is now Schema-oriented rather than type-string-oriented, so the object and array arms can recurse over properties and items. Nested null is passed through instead of being normalized to "", letting a nullable leaf reach the schema validator intact; the top-level null contract -- return null when the schema admits it, throw TypeMismatchError otherwise -- is unchanged. RequestBodyCoercer already did this traversal for request bodies, so the walk itself moves to AbstractCoercer::coerceDeclaredProperties() and ::coerceDeclaredItems(), parameterized by the recursion callback. Each coercer keeps its own leaf semantics -- TypeCoercer normalizes objects and unknown types, RequestBodyCoercer threads nullableAsType -- and RequestBodyCoercer's behavior is unchanged. Typing the callback let a stale MixedAssignment entry drop out of psalm-baseline.xml. Tests were written first and watched fail with the reported errors: Property "limit" validation failed, Property "enabled" validation failed, and Item at index 0 ... Expected type "integer", but got "string" at /0. NestedParameterCoercionRegressionTest drives the issue's four cases through full request validation; TypeCoercerNestedTypesTest covers the unit surface, including strict vs non-strict nested failure, nested null, union types at both levels, and coercion left disabled. coerce_declared_property_that_follows_an_absent_one exists because Infection escaped a continue-to-break mutant on the absent-property branch: ordering only matters when a declared property that is missing precedes one that is present.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #60.
Problem
TypeCoercer::coerceToType()has arms forinteger,number,booleanandstring;objectandarrayfall through tonormalizeValue(), which returns arrays untouched.AbstractParameterValidator::validate()then hands the un-coerced container to the schema validator, which correctly rejects"3"againsttype: integer.That is not merely strict. Query, path, header and cookie values arrive as strings, so coercion is the only mechanism by which a nested
integer/number/booleancould ever validate — againstpage: {type: object, properties: {limit: {type: integer}}}there is no value a client could send that would pass. A JSON:API style API hits this immediately onpage[limit],page[offset]and booleanfilter[...]members.This is distinct from #58:
?ids=1,2,3already deserializes to a three element array and still fails.Fix
Type dispatch in
TypeCoerceris nowSchema-oriented rather than type-string-oriented, so the newobjectandarrayarms can recurse overpropertiesanditems. Nestednullis passed through instead of being normalized to"", so anullableleaf reaches the schema validator intact. The top-levelnullcontract — returnnullwhen the schema admits it, throwTypeMismatchErrorotherwise — is unchanged.Per the issue's suggestion, the traversal is not duplicated.
RequestBodyCoerceralready walkedproperties/itemsfor request bodies, so that walk moves toAbstractCoercer::coerceDeclaredProperties()and::coerceDeclaredItems(), parameterized by the recursion callback. Each coercer keeps its own leaf semantics (TypeCoercernormalizes objects and unknown types;RequestBodyCoercerthreadsnullableAsType), andRequestBodyCoercer's behavior is unchanged — its duplicated walkers are simply gone. Typing the callback let a now-staleMixedAssignmententry drop out ofpsalm-baseline.xml.Result
The issue's repro script, verbatim:
Tests
Written first and watched fail with the reported errors (
Property "limit" validation failed,Property "enabled" validation failed,Item at index 0 … Expected type "integer", but got "string" at /0).tests/Unit/Regression/R4/NestedParameterCoercionRegressionTest.php— the issue's four cases through full request validation.tests/Unit/Validator/Request/TypeCoercerNestedTypesTest.php— the unit surface: object properties → integer / number / boolean, array items, array-of-objects, object-holding-array, undeclared members preserved, strict vs non-strict nested failure, nestednullundernullable, union types at both the container and leaf level, and coercion left disabled.coerce_declared_property_that_follows_an_absent_oneexists because Infection escaped acontinue→breakmutant on the absent-property branch: ordering only matters when a declared property that is missing precedes one that is present. Adding it took the changed files to 95% covered MSI with no escaped mutants in the new code; the four that remain sit inTypeCoercer's pre-existing top-levelnullblock and union loop, which I left alone rather than widen the diff.Checks
make tests(7150 tests, 14741 assertions, green),make psalm(no errors),make cs-fixandmake rector(both clean) per CONTRIBUTING.md.One deviation worth flagging: the two new
AbstractCoercerhelpers carry@param callable(mixed, Schema): (...)docblocks — needed for Psalm to infer through the callback — but no inline//comments, per 4e5baf8. Happy to add a// §N exemption:marker or reshape the helpers if you would rather the shared traversal live somewhere other thanAbstractCoercer.