FriendsOfSymfony / FOSRestBundle

This Bundle provides various tools to rapidly develop RESTful API's with Symfony
http://symfony.com/doc/master/bundles/FOSRestBundle/index.html
MIT License
2.8k stars 702 forks source link

Invalid json handling of empty objects {} converted to empty arrays [] #980

Open joshribakoff opened 9 years ago

joshribakoff commented 9 years ago

If you POST some json like this:

{foo:{}}

In my controller $request->get('foo') returns [] instead of stdClass {}. We're using Angular to POST to a controller that uses FOSRest.

This completely crashes our app because Angular cannot $watch an empty array.

I tried to debug & I found this:

// FOS\RestBundle\Decoder\JsonDecoder
public function decode($data)
{
        return @json_decode($data, true);
}

From the PHP docs:

mixed json_decode ( string $json [, bool $assoc = false [, int $depth = 512 [, int $options = 0 ]]] ) assoc: When TRUE, returned objects will be converted into associative arrays.

This was kind of insidious for us to debug. I came up with this override which preserves my objects:

public function decode($data)
    {
        $obj = @json_decode($data);

        $newArr = [];
        foreach($obj as $key=>$value) {
            $newArr[$key] = $value;
        }
        return $newArr;
    }

However, certain symfony components & commonly used 3rd party bundles still crash after this modification.

Currently, the only acceptable workaround we've found is to overwrite the corrupt json with valid json ourselves:

$request->get('foo); // "corrupted" by FOSRest
json_decode($request->getContent())['foo']; // this is the good stuff

I guess the one thing I could suggest you do to fix this, without breaking BC, is you could add a configuration option that puts the correct JSON in $request->get('_json') or something like that..

lsmith77 commented 9 years ago

I somehow seem to remember that we had this topic before .. at least 1-2 years past. note you can override the service used for decoding of json with a custom service.

florianv commented 9 years ago

We could maybe provide an option for the JsonDecoder to preserve objects ?

The outermost object would still need to be encoded as associative array.

lsmith77 commented 9 years ago

or we move to https://github.com/webmozart/json in case it already provides the options we need

/cc @webmozart

webmozart commented 9 years ago

If you set $assoc to true, that's what json_decode() does. You need to either set $assoc to false or convert empty arrays to objects manually.

The problem is that json_decode() is not schema-aware, but neither is webmozart/json. You could maybe create a feature request on the PHP bug tracker?

joshribakoff commented 9 years ago

To be clear, this is not a PHP bug. PHP can preserve my data just fine.

$str = "{foo:[], bar:{}}";
var_dump(json_encode(json_decode($str)) === $str); // true

json_decode() works correctly by default, FOSRestBundle is explicitly telling json_decode() to 'destroy' the data's schema. json_decode does not "change" variable types, unless you specifically ask for it to.

Unfortunately I think the problem is more complex than simply modifying the JsonDecoder service for FOSRestBundle.

After fixing FOSRest, we still have other bundles that are casting objects to arrays in various points of the Symfony lifecycle:

Hopefully you can see what I'm saying. We'd have to change the whole ecosystem, not just the FOSRestBundle.

Casting objects to arrays it would seem is a PHP "standard" or convention. What I ended up doing was casting the property back to an object in Angular side. Knowing that the PHP world is out to destroy my data (being facetious), the path of least resistance was to force an Angular coding style disallowing empty objects on the scope in the first place. I just wanted to open the issue for public discussion to see if I was overlooking anything, though.

joshribakoff commented 9 years ago

I'm thinking out loud about a solution --

Authors of PHP libraries want to write $foo['bar'] and not write $foo->bar, so they just cast all incoming json to an associative array. JS has no associative arrays [1], so PHP just casts non numerically indexed arrays to objects on the response.

[1] - (Writing foo['bar'] just creates an object in JS, as if you'd written foo.bar)

This works -- JS sends an object, PHP converts to an array, processes it, converts it back to an object, JS gets an object back... Except it does not work, in the case of empty objects....

As an Angular developer, I want my backend to properly persist my JSON verbatim. Even for edge cases like empty objects. So what if my backend substituted empty objects with a "value array"? Confused?

Basically you need a request listener that:

Any libraries like SyliusResourceBundle can just do $request->get('foo') and get back the array ['_blank_object'=>true] (instead of getting an object & failing a type check, etc.)

Then you'd have a response listener for when FOSRest is serializing a json response:

In theory, all the naughty PHP libraries can hook into the symfony lifecycle & cast my data to an array all they want without an issue now. They can remain ignorant as to the array's secret meaning. It is like passing a "trojan horse" array through symfony, by disguising my object as an array... since PHP devs like arrays, apparently. lol.

Could something like this be done through the existing hooks that FOS Rest provides? By writing custom encoders & decoder services that implement my idea? Would it be "hacky"?

lsmith77 commented 9 years ago

maybe it would be safer to just add a flag on the request, like $request->attributes->set('_blank_object', true); which the view handler can then be made aware of

sela commented 5 years ago

Is this issue still open? I got the same issue in my code getting an empty object {} from React client that is being transformed to []. I guess I got no choice, but write a listener.

cia05rf commented 4 years ago

OK, i've just found this thread and i think the response is a simple one. The documentation outlines how json_encode cna be used to force empty arrays to be converted to objects (docs here).

It's as simple as: json_encode($b, JSON_FORCE_OBJECT)

dipankarjain commented 4 years ago

@cia05rf That would typecast even the arrays sent in the json payload to object. We would ideally want to preserve all types sent in the payload.