FOSRestBundle icon indicating copy to clipboard operation
FOSRestBundle copied to clipboard

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

Open joshribakoff opened this issue 9 years ago • 10 comments

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..

joshribakoff avatar Feb 28 '15 07:02 joshribakoff

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.

lsmith77 avatar Mar 01 '15 11:03 lsmith77

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

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

florianv avatar Mar 01 '15 16:03 florianv

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

/cc @webmozart

lsmith77 avatar Mar 02 '15 08:03 lsmith77

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?

webmozart avatar Mar 02 '15 08:03 webmozart

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:

  • Doctrine has concept of entity having a property that is a "json array" - casts data to array when loading entities, causes problems if you pass in an object...
  • When using the SyliusResourceBundle (which depends on FOSRest) it passes the request to the Symfony form which also crashes if it gets an array not an object.
  • JMSSerializerBundle also is configured by default to cast objects to arrays when creating the response...
  • etc... every bundle that depends on FOSRest expects an array always now.
  • Symfony\Forms cannot handle objects
  • Symfony\Request object can only handle arrays
  • I'd imagine lots of other Symfony components & 3rd party bundles follow this pattern

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 avatar Mar 02 '15 13:03 joshribakoff

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:

  • leaves the incoming array [] as []
  • converts any empty object {} to a request param w/ the (array) value ['_blank_object'=>true]

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:

  • leaves the array [] as []
  • converts the array ['_blank_object'=>true] back to a blank object {}

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"?

joshribakoff avatar Mar 02 '15 15:03 joshribakoff

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

lsmith77 avatar Apr 10 '15 07:04 lsmith77

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.

sela avatar Nov 12 '18 12:11 sela

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)

cia05rf avatar Mar 31 '20 09:03 cia05rf

@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.

dipankarjain avatar May 15 '20 11:05 dipankarjain