-
-
Notifications
You must be signed in to change notification settings - Fork 586
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
Add control to deserialization of null values #821
Comments
I guess here a custom setter here should already solve the issue /**
* @var ArrayCollection
* @JMS\Type("ArrayCollection<MyObject>")
* @Accessor(setter="setCollection")
*/
protected $collection;
public function __construct()
{
$this->collection = new ArrayCollection();
}
public function setCollection($value)
{
$this->collection = $value instanceof ArrayCollection? $value : new ArrayCollection();
} Am I missing something? |
@goetas , thank you for your reply. Yes, that would obviously work, but it would mean a check in each setter for each single variable which is of that type(s). |
Delegating it to handlers technically might work, but will make more complex the handler logic as they will have to handle explicitly the null/not-null values (by some configuration that will need to be exposed somehow)
callbacks are exactly what is my solution proposed before. default values will not work as they need some logic (constructor args as example) and to do so we will need some code to be somewhere... that will end-up in a callback... |
The difference is that using a callback this way we would need as many callbacks as properties of that type. Default values can already be set via the constructor setting the ObjectConstructor in the SerializerBuilder. |
hmm... something as
might be a nice idea. what do you think? |
To be very honest this seems to me like a bit of misuse of the |
What do you want to add in the DeserializationContext ? probably I'm not understanding your solution as inside it you have to encode some logic (and DeserializationContext is not meant for it) Can you make me an example? |
What currently happens in the GenericDeserializationVisitor, in visitProperty is this: $v = $data[$name] !== null ? $this->navigator->accept($data[$name], $metadata->type, $context) : null;
$this->accessor->setValue($this->currentObject, $v, $metadata); That is, if the provided data is null, the value is always set to null. The point is that in that method we have access to the Context. if ($context->skipDeserializeNull() === false) {
$this->accessor->setValue($this->currentObject, $v, $metadata);
} This way, if we have already set default values via the constructor, they would be untouched. What do you think? |
sorry to disappoint you but the constructor is never called by the jms serializer. the object instantiation is done via reflection |
I know the constructor is not called, but in the SerializerBuilder it is possible to set the ObjectConstructor. and what we currently inject is a SimpleObjectConstructor which calls the constructor. |
Calling the constructor in the serialization process is a bad idea. the constructor should be called only in your business logic |
The constructor is called in our implementation of the ObjectConstructorInterface, maybe my explanation was not clear :) |
expression language inside a custom accessor might look an abuse but imo has less side-effects (it will work just as a callback before the "set") |
I don't understand how having more options can be a problem :) Besides, I think the class MyClass{
/**
* @var int
* @JMS\Type("int")
*/
protected $myProperty = 123;
} |
The problem is the delicate balance between features and stability, obtained via options and constraints... but i see your point here @schmittjoh what is your opinion in this case? |
There is a much cleaner solution (in my opinion). The real problem is that the serializer skips the constructor because it uses reflection to set properties. Using the context 'target' we can set a empty object as a starting point which ofcourse goes through the constructor during instantiation. Then when the serializer fills the object it will ignore the null value and keep the objects original constructor value.
<?php
/*
* Copyright 2016 Johannes M. Schmitt <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MyNamespace\Serializer;
use JMS\Serializer\VisitorInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Construction\ObjectConstructorInterface;
/**
* Object constructor that allows deserialization into already constructed
* objects passed through the deserialization context
*/
class InitializedObjectConstructor implements ObjectConstructorInterface
{
private $fallbackConstructor;
/**
* Constructor.
*
* @param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
*/
public function __construct(ObjectConstructorInterface $fallbackConstructor)
{
$this->fallbackConstructor = $fallbackConstructor;
}
/**
* {@inheritdoc}
*/
public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context)
{
if ($context->attributes->containsKey('target') && $context->getDepth() === 1) {
return $context->attributes->get('target')->get();
}
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
}
use JMS\Serializer\SerializerBuilder;
use JMS\Serializer\Construction\UnserializeObjectConstructor;
use JMS\Serializer\DeserializationContext;
use MyNamespace\InitializedObjectConstructor;
$fallback = new UnserializeObjectConstructor();
$objectConstructor = new InitializedObjectConstructor($fallback);
$serializer = SerializerBuilder::create()
->setObjectConstructor($objectConstructor)
->build();
$context = DeserializationContext::create();
$context->attributes->set('target', new MyClass());
$object = $serializer->deserialize($requestBody, MyClass, 'json', $context); edited, excuse my late-night-writing forgetfulness, I have added the missing configuration and custom class |
It is quite funny that deserializing array of nulls has opposite effect (nulls are never preserved): $serializer = \JMS\Serializer\SerializerBuilder::create()->build();
var_dump($serializer->fromArray(['item' => null], 'array<string, stdClass>'));
As you probably already have guessed, my needs are straight opposite to that (i would like to preserve nulls) |
fixed in 94c319d |
Hmm, you might be right. can #1005 be a solution for it? |
@goetas Thanks for your work on this lib =) #1005 indeed seems related (but I can't speak for others); for example, it would give $serializer = \JMS\Serializer\SerializerBuilder::create()->build();
var_dump($serializer->fromArray(['item' => null], 'array<string, stdClass>'));
and $serializer = \JMS\Serializer\SerializerBuilder::create()->build();
$dCtx = \JMS\Serializer\DeserializationContext::create()->setDeserializeNull(true);
var_dump($serializer->fromArray(['item' => null], 'array<string, stdClass>', $dCtx));
(don't know for the |
Given a class which has a collection or an array, like for instance:
and serialized data such as
['collection' => null]
, what currently happens when the data is deserialized, is that the property$collection
becomes null rather than, for instance, an empty collection.It would be nice to be able to control the value which is being set when a
null
input value is found.Some possible solutions :
DeserializationContext
to determine whether to set the value to null or notPlease let us know your thoughts and we would like to contribute.
Thank you!
The text was updated successfully, but these errors were encountered: