11

I'm trying to return a JSON object from one of my REST Models, something like this:

{
    "settings": {
        "set1" : 2,
        "set2" : "key1" 
    },
    "extra": {
        "e1'" : {
            "e2'": true 
        }
    }
}

However, what seemingly looks trivial, is not that easy to implement. The problem is I'm not sure what the return type should be in the interface and model.

<?php

namespace AppFactory\Core\Api;

/**
 * @api
 */

interface SettingsInterface
{


    /**
     * @return object
     */
    public function get();
}

The object class will return

{
  "message": "Class object does not exist",

when calling the API. The primitive types available int, number, and array won't work for me. I don't want to create a class for each complex type is returning as well. How can I do this?

Thanks.

3
  • json data is string for php so make it string Jun 3, 2017 at 15:24
  • @MohammadMujassam return string in the docBlock will make Magento convert the output object to string escaping the " with backslashes and surrounding the entire object with ". I went through this article maxchadwick.xyz/blog/… and it suggests there is no other ways of returning an object other than to create a data model for it, but I just want to make sure this is the only way and there are no other ways. Jun 3, 2017 at 15:31
  • yeah definitely, it will. Jun 3, 2017 at 17:09

5 Answers 5

21
+50

I am assuming that AppFactory\Core\Api\SettingInterface::get() is a REST endpoint. In that case in phpdoc comments you need to define what this will return. Magento REST handler will take that value and process it to remove all data that are unnecessary. What's left will be encoded into JSON so in javascript you can retrieve it as already proper JS hash and not json-encoded string.

The trick about those endpoint is that you need to define very precisely what will you return. Magento will not be able to process something as general as "array" where you will set whatever you like.

In your case, in order not to try playing with array of strings, it will be easier to create an interface that your endpoint will return.

 <?php

 namespace AppFactory\Core\Api;

 /**
  * @api
  */

 interface SettingsInterface
 {


     /**
      * @return Data\SettingsInterface
      */
     public function get();
 }

Now when you return an instance of an object implementing that interface Magento will read its phpdocs and will process their return values. Now create a file in AppFactory\Core\Api\Data\SettingsInterface as follows

<?php

namespace AppFactory\Core\Api\Data;

interface SettingsInterface
{
    /**
    * @return int[]
    **/
    public function getSettings();

    /**
    * @return string[]
    **/
    public function getExtra();
}

Now when you create actual class that will implement those 2 get methods and you will return it in AppFactory\Core\Api\SettingsInterface::get() then magento will return something like

{
    "settings": [1, 2, 5],
    "extra": ["my","array","of","strings"]
}

If you want another level you need to create another interface which will keep settings structure and add it as a return value for AppFactory\Core\Api\Data\SettingsInterface::getSettings().

If you need to have something that will be dynamic and you do not want or can't prepare this structure interface then you can try setting json-encoded string and place @return string for any of the fields. This way however you will have to make sure to manually decode that string after receiving the response as then your response will look like this:

{
    "settings": [1, 2, 5],
    "extra": "{\"test\":\"string\",\"value\":8}"
}

and in order to use response.extra.test you will have to first do response.extra = JSON.parse(response.extra); manually

11
  • thanks for the detailed explanation, decoding the string on the client side does not feel natural, and writing all the classes to represent each item is a nightmare, is there any fourth option to just return the json object without having to write the classes or returning string, maybe the return mixed annotation, though i tried it but didnt work unfortunately. Looks like i'll end up enclosing the final json in an array e.g. array( $json_object), this will do the trick, but does not feel natural as well, having to pick up the first item in the array from the client side Jun 7, 2017 at 20:40
  • You can create a regular controller action which will return json string and set header to text/json or application/json and it will be decoded on the browser side. If you want to use rest api then I did not find anything that could bypass that post processing.
    – Zefiryn
    Jun 7, 2017 at 20:45
  • yea looks like it, magento should support this in some way and loosen up without enforcing this type of validation on the return type Jun 7, 2017 at 20:47
  • @YehiaA.Salam I was checking something. I have no easy way of testing this but try to use "mixed" as a return on methods in AppFactory\Core\Api\DataSettingsInterface. If this works you only need to do first level of the response.
    – Zefiryn
    Jun 8, 2017 at 22:10
  • Very usefull answer Oct 23, 2017 at 13:48
8

I've also faced this problem, and as an alternative to the solution @Zefiryn proposed, I have worked around it by enclosing the return data in an array (or two). Please consider the example below.

/**
 * My function
 *
 * @return
 */
public function myFunction()
{
  $searchCriteria = $this->_searchCriteriaBuilder->addFilter('is_filterable_in_grid',true,'eq')->create();
  $productAttributes = $this->_productAttributeRepository->getList($searchCriteria)->getItems();

  $productAttributesArray = [];
  foreach ($productAttributes as $attribute) {
    $productAttributesArray[$attribute->getAttributeCode()] = $this->convertAttributeToArray($attribute);
  }

  return [[
          "attributes"=>$productAttributesArray,
          "another_thing"=>["another_thing_2"=>"two"]
        ]];
}

private function convertAttributeToArray($attribute) {
  return [
    "id" => $attribute->getAttributeId(),
    "code" => $attribute->getAttributeCode(),
    "type" => $attribute->getBackendType(),
    "name" => $attribute->getStoreLabel(),
    "options" => $attribute->getSource()->getAllOptions(false)
  ];
}

Due to how Magento 2 allows arrays of mixed content as return values, more complex data structures can be embedded inside other arrays. The sample above yields the following JSON response (truncated for readability).

[
{
    "attributes": {
        "special_price": {
            "id": "78",
            "code": "special_price",
            "type": "decimal",
            "name": "Special Price",
            "options": []
        },
        "cost": {
            "id": "81",
            "code": "cost",
            "type": "decimal",
            "name": "Cost",
            "options": []
        },
    "another_thing": {
        "another_thing_2": "two"
    }
}
]

Enclosing it in a single layer removes the keys of the array, and without enclosing it in any array results in an error.

Understandably none of this is ideal, but this approach allows me to control the consistency in the returned data structure to a certain degree (the expected data structure and types). If you are also in control of writing a client-side library, an interceptor can be implemented to strip the outer array before returning it to the application.

1
  • If it's stupid and it works... wrapping it in doubled-up array brackets worked for me. Oct 5, 2021 at 13:21
7

I know this question is quite old, but there is one quite simple solution for this:

You either need to replace the Json-Renderer Magento\Framework\Webapi\Rest\Response\Renderer\Json or you write a Plugin for it.

Here a little example of a plugin:

In your di.xml

<type name="Magento\Framework\Webapi\Rest\Response\Renderer\Json">
    <plugin name="namespace_module_renderer_json_plugin" type="Namespace\Module\Plugin\Webapi\RestResponse\JsonPlugin" sortOrder="100" disabled="false" />
</type>

In your new Plugin-Class Namespace\Module\Plugin\Webapi\RestResponse\JsonPlugin

<?php
namespace Namespace\Module\Plugin\Webapi\RestResponse;

use Magento\Framework\Webapi\Rest\Request;
use Magento\Framework\Webapi\Rest\Response\Renderer\Json;

class JsonPlugin
{

    /** @var Request */
    private $request;

    /**
     * JsonPlugin constructor.
     * @param Request $request
     */
    public function __construct(
        Request $request
    )
    {
        $this->request = $request;
    }

    /**
     * @param Json $jsonRenderer
     * @param callable $proceed
     * @param $data
     * @return mixed
     */
    public function aroundRender(Json $jsonRenderer, callable $proceed, $data)
    {
        if ($this->request->getPathInfo() == "/V1/my/rest-route" && $this->isJson($data)) {
            return $data;
        }
        return $proceed($data);
    }

    /**
    * @param $data
    * @return bool
    */
    private function isJson($data)
    {
       if (!is_string($data)) {
       return false;
    }
    json_decode($data);
    return (json_last_error() == JSON_ERROR_NONE);
}

}

What happens here:

  • If the rest-route is "/V1/my/rest-route", then the new rendering-method is used, which means simply, that the data is not encoded.
  • An additional check-method is used to evaluate if the string is really a json-object. Otherwise (for instance, if the response is an 401-error, is would result in an internal error and give back a wrong status code)
  • This way, in your rest-method, you can give back a json-string, which will not be changed.

Of course you can also write your own Renderer, which processes an array for instance.

0

For Magento 2.3.1, if you need to bypass the array serialization you can check this file to update the core logic. I think it is a good entry point. But by doing this you will break the Soap compatibility for sure.

Moreover on Magento 2.1.X, you do not have this issue if you put anyType as return type.

Github reference : https://github.com/magento/magento2/blob/2.3-develop/lib/internal/Magento/Framework/Reflection/TypeCaster.php

Commit change reference : https://github.com/magento/magento2/commit/6ba399cdaea5babb373a35e88131a8cbd041b0de#diff-53855cf24455a74e11a998ac1a871bb8

vendor/magento/framework/Reflection/TypeCaster.php:42

     /**
     * Type caster does not complicated arrays according to restrictions in JSON/SOAP API
     * but interface and class implementations should be processed as is.
     * Function `class_exists()` is called to do not break code which return an array instead
     * interface implementation.
     */
    if (is_array($value) && !interface_exists($type) && !class_exists($type)) {
        return $this->serializer->serialize($value);
    }

And replace by :

     /**
     * Type caster does not complicated arrays according to restrictions in JSON/SOAP API
     * but interface and class implementations should be processed as is.
     * Function `class_exists()` is called to do not break code which return an array instead
     * interface implementation.
     */
    if (is_array($value) && !interface_exists($type) && !class_exists($type)) {
        return $value;
    }
1
0

I faced the same problem and it took me a while to figure out the problem.

Magento does something weird in there web api service output processor which is located under Magento\Framework\Webapi\ServiceOutputProcessor In this class there is a method named convertValue(); which is the reason for the [] braces.

The best solution for me to solve the problem was to create a around plugin to overcome this if condition in the convertValue(); method where they check if $data is an array and do that weird stuff with it.

Here is my example code of the plugin: I think every one knows how to create a basic Magento 2 module, so I only post the code of the plugin itself here.

  • Create a Plugin folder

  • Create a class Vendor\ModuleName\Plugin\ServiceOutputProcessorPlugin.php

<?php

namespace Vendor\ModuleName\Plugin;

use Magento\Framework\Webapi\ServiceOutputProcessor;

class ServiceOutputProcessorPlugin
{
    public function aroundConvertValue(ServiceOutputProcessor $subject, callable $proceed, $data, $type)
    {
        if ($type == 'array') {
            return $data;
        }
        return $proceed($data, $type);
    }
}
  • Create the Plugin declaration in Vendor\ModuleName\etc\di.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Webapi\ServiceOutputProcessor">
        <plugin name="vendor_modulenameplugin" type="Vendor\ModuleName\Plugin\ServiceOutputProcessorPlugin"/>
    </type>
</config>

This should solve the array json output problem in the web api

Hope this helps

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.