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

Fix stdClass inconsistencies when serializing to JSON #730

Merged
merged 4 commits into from
Apr 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/JMS/Serializer/GenericSerializationVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
* @deprecated
*/
abstract class GenericSerializationVisitor extends AbstractVisitor
{
private $navigator;
Expand Down
4 changes: 2 additions & 2 deletions src/JMS/Serializer/Handler/FormErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use JMS\Serializer\YamlSerializationVisitor;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\GenericSerializationVisitor;
use JMS\Serializer\VisitorInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormError;
use Symfony\Component\Translation\TranslatorInterface;
Expand Down Expand Up @@ -125,7 +125,7 @@ private function getErrorMessage(FormError $error)
return $this->translator->trans($error->getMessageTemplate(), $error->getMessageParameters(), 'validators');
}

private function convertFormToArray(GenericSerializationVisitor $visitor, Form $data)
private function convertFormToArray(VisitorInterface $visitor, Form $data)
{
$isRoot = null === $visitor->getRoot();

Expand Down
219 changes: 196 additions & 23 deletions src/JMS/Serializer/JsonSerializationVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,63 +19,236 @@
namespace JMS\Serializer;

use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Metadata\PropertyMetadata;

class JsonSerializationVisitor extends GenericSerializationVisitor
{
private $options = 0;

public function getResult()
private $navigator;
private $root;
private $dataStack;
private $data;

public function setNavigator(GraphNavigator $navigator)
{
$result = @json_encode($this->getRoot(), $this->options);
$this->navigator = $navigator;
$this->root = null;
$this->dataStack = new \SplStack;
}

switch (json_last_error()) {
case JSON_ERROR_NONE:
return $result;
/**
* @return GraphNavigator
*/
public function getNavigator()
{
return $this->navigator;
}

case JSON_ERROR_UTF8:
throw new \RuntimeException('Your data could not be encoded because it contains invalid UTF8 characters.');
public function visitNull($data, array $type, Context $context)
{
return null;
}

default:
throw new \RuntimeException(sprintf('An error occurred while encoding your data (error code %d).', json_last_error()));
public function visitString($data, array $type, Context $context)
{
if (null === $this->root) {
$this->root = $data;
}

return (string) $data;
}

public function getOptions()
public function visitBoolean($data, array $type, Context $context)
{
return $this->options;
if (null === $this->root) {
$this->root = $data;
}

return (boolean) $data;
}

public function setOptions($options)
public function visitInteger($data, array $type, Context $context)
{
$this->options = (integer) $options;
if (null === $this->root) {
$this->root = $data;
}

return (int) $data;
}

public function visitDouble($data, array $type, Context $context)
{
if (null === $this->root) {
$this->root = $data;
}

return (float) $data;
}

/**
* @param array $data
* @param array $type
* @param Context $context
* @return mixed
*/
public function visitArray($data, array $type, Context $context)
{
$result = parent::visitArray($data, $type, $context);
$this->dataStack->push($data);

$isHash = isset($type['params'][1]);

if (null !== $this->getRoot() && isset($type['params'][1]) && 0 === count($result)) {
// ArrayObject is specially treated by the json_encode function and
// serialized to { } while a mere array would be serialized to [].
return new \ArrayObject();
if (null === $this->root) {
$this->root = $isHash ? new \ArrayObject() : array();
$rs = &$this->root;
} else {
$rs = $isHash ? new \ArrayObject() : array();
}

$isList = isset($type['params'][0]) && ! isset($type['params'][1]);

foreach ($data as $k => $v) {
$v = $this->navigator->accept($v, $this->getElementType($type), $context);

if (null === $v && $context->shouldSerializeNull() !== true) {
continue;
}

if ($isList) {
$rs[] = $v;
} else {
$rs[$k] = $v;
}
}

return $result;
$this->dataStack->pop();
return $rs;
}

public function startVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
{
if (null === $this->root) {
$this->root = new \stdClass;
}

$this->dataStack->push($this->data);
$this->data = array();
}

public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
{
$rs = parent::endVisitingObject($metadata, $data, $type, $context);
$rs = $this->data;
$this->data = $this->dataStack->pop();

// Force JSON output to "{}" instead of "[]" if it contains either no properties or all properties are null.
if (empty($rs)) {
$rs = new \ArrayObject();
}

if (array() === $this->getRoot()) {
$this->setRoot(clone $rs);
}
if ($this->root instanceof \stdClass && 0 === $this->dataStack->count()) {
$this->root = $rs;
}

return $rs;
}

public function visitProperty(PropertyMetadata $metadata, $data, Context $context)
{
$v = $this->accessor->getValue($data, $metadata);

$v = $this->navigator->accept($v, $metadata->type, $context);
if (null === $v && $context->shouldSerializeNull() !== true) {
return;
}

$k = $this->namingStrategy->translateName($metadata);

if ($metadata->inline) {
if (is_array($v)) {
$this->data = array_merge($this->data, $v);
}
} else {
$this->data[$k] = $v;
}
}

/**
* Allows you to add additional data to the current object/root element.
* @deprecated use setData instead
* @param string $key
* @param integer|float|boolean|string|array|null $value This value must either be a regular scalar, or an array.
* It must not contain any objects anymore.
*/
public function addData($key, $value)
{
if (isset($this->data[$key])) {
throw new InvalidArgumentException(sprintf('There is already data for "%s".', $key));
}

$this->data[$key] = $value;
}

/**
* Checks if some data key exists.
*
* @param string $key
* @return boolean
*/
public function hasData($key)
{
return isset($this->data[$key]);
}

/**
* Allows you to replace existing data on the current object/root element.
*
* @param string $key
* @param integer|float|boolean|string|array|null $value This value must either be a regular scalar, or an array.
* It must not contain any objects anymore.
*/
public function setData($key, $value)
{
$this->data[$key] = $value;
}

public function getRoot()
{
return $this->root;
}

/**
* @param array|\ArrayObject $data the passed data must be understood by whatever encoding function is applied later.
*/
public function setRoot($data)
{
$this->root = $data;
}


public function getResult()
{
$result = @json_encode($this->getRoot(), $this->options);

switch (json_last_error()) {
case JSON_ERROR_NONE:
return $result;

case JSON_ERROR_UTF8:
throw new \RuntimeException('Your data could not be encoded because it contains invalid UTF8 characters.');

default:
throw new \RuntimeException(sprintf('An error occurred while encoding your data (error code %d).', json_last_error()));
}
}

public function getOptions()
{
return $this->options;
}

public function setOptions($options)
{
$this->options = (integer) $options;
}
}
2 changes: 1 addition & 1 deletion src/JMS/Serializer/Serializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private function handleDeserializeResult($visitorResult, $navigatorResult)

private function convertArrayObjects($data)
{
if ($data instanceof \ArrayObject) {
if ($data instanceof \ArrayObject || $data instanceof \stdClass) {
$data = (array) $data;
}
if (is_array($data)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ public function testSerializeWithNonUtf8EncodingWhenDisplayErrorsOn()

public function testSerializeArrayWithEmptyObject()
{
$this->assertEquals('{"0":{}}', $this->serialize(array(new \stdClass())));
$this->assertEquals('[{}]', $this->serialize(array(new \stdClass())));
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this to me sounds a wrong test case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@schmittjoh What do you think?

}

public function testSerializeRootArrayWithDefinedKeys()
Expand Down Expand Up @@ -350,6 +350,8 @@ public function getTypeHintedArraysAndStdClass()

return [

[[$c1], '[{}]', SerializationContext::create()->setInitialType('array<stdClass>')],

[[$c2], '[{"foo":"bar"}]', SerializationContext::create()->setInitialType('array<stdClass>')],

[[$tag], '[{"name":"tag"}]', SerializationContext::create()->setInitialType('array<JMS\Serializer\Tests\Fixtures\Tag>')],
Expand Down