Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow union deserialization using discriminator map #1553

Merged
merged 15 commits into from
Aug 18, 2024

Conversation

idbentley
Copy link
Contributor

Q A
Bug fix? no
New feature? yes
Doc updated no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #1549
License MIT

Hi @goetas here's a simpler version of PR #1549 - this one only adds support for discriminated complex unions.

@idbentley idbentley mentioned this pull request Jul 26, 2024
@scyzoryck scyzoryck requested a review from goetas July 30, 2024 10:16
@idbentley
Copy link
Contributor Author

Hi @goetas - I wonder if you've had a chance to look at these changes?

Copy link
Collaborator

@goetas goetas left a comment

Choose a reason for hiding this comment

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

hi, sorry for the delay in my review. i have left some comments, could you answer my questions please?

after you answer to #1553 (comment), could you please update the documentation as well? most of what you need is to update /doc/reference/annotations.rst

if (null !== $finalType && null !== $finalType['name']) {
return $context->getNavigator()->accept($data, $finalType);
} else {
foreach ($type['params'] as $possibleType) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

something is odd with this foreach loops, you have here a nested loop the same variable (line 70 and 102). shouldn't they be independent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was an error - removing the inner loop is the correct fix.

}
} else {
$finalType = [
'name' => $lkup,
Copy link
Collaborator

@goetas goetas Aug 8, 2024

Choose a reason for hiding this comment

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

i think that is an anti pattern to leak class name into the JSON representation.... it might be a "quick thing" but it has backfired at me so many times...
It can represent also potential security issues as it allows third parties to instantiate potentially any class.
are you using this approach? (as example in the "inheritance" discriminator map this feature is intentionally requiring the discriminator map to be not empty)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I understand the alternative to putting the className into the type.

This can happen in many ways. The simplest I can think of is:

class SomeClass {
    private MappedDiscriminatedAuthor $data
}

will result in such a type:

$type = ['name' => 'MappedDiscriminatedAuthor', 'params' => []]

I think I must be misunderstanding your question.

@@ -102,6 +102,10 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
foreach ($classMetadata->propertyMetadata as $propertyMetadata) {
// If the inner driver provides a type, don't guess anymore.
if ($propertyMetadata->type) {
if ('union' === $propertyMetadata->type['name']) {
$propertyMetadata->setType($this->reorderTypes($propertyMetadata->type));
Copy link
Collaborator

Choose a reason for hiding this comment

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

you could potentially encode in the union type already the info regarding the discriminator field and map.
advantage of it is that it would be done only once at compile cache metadata time vs at runtime as done now.
it should be much faster.

#[UnionDiscriminator(
  field: 'objectType', 
  map: [
   'author' => 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor', 
   'comment' => 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment'
  ]
)]

can be translated to

[
  'type' => 'union',
  'params' => [
      'objectType',
       [
        'author' => 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedAuthor', 
        'comment' => 'JMS\Serializer\Tests\Fixtures\MappedDiscriminatedComment'
       ]

  ]
]

in this way you have all the data ready in the UnionHandler and no need to fetch metadata each time

do you see it as doable approach?

Copy link
Contributor Author

@idbentley idbentley Aug 12, 2024

Choose a reason for hiding this comment

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

I'm not sure I understand what you're asking for, so I put together a commit (34439ca) that contains your ask as I understand it.

Please see the most recent commit for the change in full. Basically, my change does the following.

If a UnionDiscriminator annotation exists, then the params array on the type will be overwritten:

from

[
    'name' => 'union',
    'params' => [['name' => 'Path\Type1', 'params' => []], ['name' => 'Path\Type2', 'params' => []]
]

to

[
    'name' => 'union',
    'params' => ['type']
]

If the UnionDiscriminator annotation contains a field and a map, both are included:

[
    'name' => 'union',
    'params' => ['type', ['type1' => 'Path\Type1', ...]]
]

The UnionHandler can handle a params list that contains any of these combinations:

  1. A param list of possibleTypes (if they are primitives, they will resolve if the data matches).
  2. A param list with just a field name - the parameter with a matching name will be used to find the resolution type.
  3. A param list with a field name and a lookup map. The parameter with a matching name will be used to lookup the final type from the lookup map.

@goetas
Copy link
Collaborator

goetas commented Aug 12, 2024

Your latest version is exactly what I had in mind, nice!

The only thing left in my opinion is to address the potential security issue. With your implementation,

class ComplexDiscriminatedUnion
{
    #[UnionDiscriminator(field: 'type')]
    private DiscriminatedAuthor|DiscriminatedComment $data;

If a user sends this JSON

{
 "type":  "DOMDocument"
}

with the current implementation the serializer will attempt to instantiate a DOMDocument class and to set it to $data.

In this case DOMDocument is harmless, but any class can be passed there.

The solution here is to have the map parameter strictly necessary and only the classes in the map should be used (this is the same solution used for the inheritance version)

@idbentley
Copy link
Contributor Author

@goetas thanks for the feedback. I think I captured all the changes you asked for!

} else {
throw new NonVisitableTypeException('Union Discriminator Map does not contain key \'' . $lkup . '\'');
}
} else {
Copy link
Collaborator

@goetas goetas Aug 13, 2024

Choose a reason for hiding this comment

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

this else branch still exposes the ability to pass any class name in the json and later that class will be instantiated. I think that this should be removed

goetas added 2 commits August 13, 2024 23:16
…alizer into idbentley-union-types-discriminated
- use early returns
- simplify the reordering of union types
@goetas
Copy link
Collaborator

goetas commented Aug 13, 2024

@idbentley I took the liberty of pushing the changes my self, could you please have a look?

@goetas goetas requested a review from scyzoryck August 13, 2024 22:08
@idbentley
Copy link
Contributor Author

@goetas Your changes look good to me!

@idbentley
Copy link
Contributor Author

I'm going to be OOTO next week. Feel free to proceed without me!

@@ -177,6 +177,16 @@ public function accept($data, ?array $type = null)

throw new RuntimeException($msg);

case 'union':
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just a thought - as the logic in growing and most probably is becoming a first class citizen - IMHO using handler might be redundant here. I guess we might go with direct implementation in GraphNavigator in the future. If we need additional logic here, having handler seems to be redundant.

/**
* @var string|null
*/
public $unionDiscriminatorField;
Copy link
Collaborator

Choose a reason for hiding this comment

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

When we moved the code to the type, those fields seems to be not needed anymore. :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

hmm, can you please clarify? im not sure I understand

@goetas goetas changed the title Union types discriminated Allow union deserialization using discriminator map Aug 18, 2024
@goetas goetas merged commit b71da8c into schmittjoh:master Aug 18, 2024
16 of 18 checks passed
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