{"id":15529046,"url":"https://github.com/alexander-schranz/do-we-really-need-a-serializer","last_synced_at":"2026-05-04T02:32:46.145Z","repository":{"id":88923314,"uuid":"454574283","full_name":"alexander-schranz/do-we-really-need-a-serializer","owner":"alexander-schranz","description":"An article about alternative solution for convert object into a JSON Object for your api.","archived":false,"fork":false,"pushed_at":"2022-02-01T23:00:09.000Z","size":17,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-28T21:43:23.090Z","etag":null,"topics":["alexander-schranz-article","api","json","php","serializer"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexander-schranz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-02-01T22:38:33.000Z","updated_at":"2022-02-01T23:31:25.000Z","dependencies_parsed_at":"2023-06-13T04:15:32.541Z","dependency_job_id":null,"html_url":"https://github.com/alexander-schranz/do-we-really-need-a-serializer","commit_stats":{"total_commits":2,"total_committers":1,"mean_commits":2.0,"dds":0.0,"last_synced_commit":"d15e25813c65a58bdc67465a67187d35fdc96b51"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/alexander-schranz/do-we-really-need-a-serializer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexander-schranz%2Fdo-we-really-need-a-serializer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexander-schranz%2Fdo-we-really-need-a-serializer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexander-schranz%2Fdo-we-really-need-a-serializer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexander-schranz%2Fdo-we-really-need-a-serializer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexander-schranz","download_url":"https://codeload.github.com/alexander-schranz/do-we-really-need-a-serializer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexander-schranz%2Fdo-we-really-need-a-serializer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32592477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alexander-schranz-article","api","json","php","serializer"],"created_at":"2024-10-02T11:16:04.942Z","updated_at":"2026-05-04T02:32:46.126Z","avatar_url":"https://github.com/alexander-schranz.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Do we really need a serializer for our JSON API?\n\nThe last years I did build a lot of JSON APIs but personally was\nnever happy about the magic of using a Serializer and lately I did\nthink more and more about do I really need a Serializer for my API.\n\n## The current state\n\nMostly I did use the [JMS Serializer](https://github.com/schmittjoh/serializer) \nand lately did experiment with the [Symfony Serializer](https://symfony.com/doc/current/components/serializer.html).\n\nWhy I think using a Serializer does a great Job for Prototyping\nor where you provide your entities over single endpoint. But can\nbe a pain if your entities are provided over several endpoints.\nIn my case I mostly have an own API for the [Sulu CMS Admin](https://github.com/sulu/sulu)\nthen maybe an additional API for the Website and in some cases\nthere were also additional APIs for an App and an Extranet.\nAll APIs did look a little different but provide data of the\nsame entity.\n\nAs having different APIs we needed to work with Serialization Groups\nand also then side effects could be possible when no Group is specified,\nand it did add accidentally properties you don't want to provide on another\nAPI. So the solution in this case was that we did define by default\nthat we always need to use a Group and we always exclude all properties\nby default via [ExclusionPolicy](https://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies):\n\n```php\nuse JMS\\Serializer\\Annotation\\ExclusionPolicy;\nuse JMS\\Serializer\\Annotation\\Groups;\n\n/**\n * @ExclusionPolicy(\"all\")\n */\nclass Article {} \n```\n\nSo now if a property was added, we explicit need to define in which serialization\ngroup the properties should be serialized.\n\n```php\n/** @Groups({\"admin\", \"website\"}) */\nprivate string $title;\n```\n\nTo avoid sideeffects between different entities we also decide to prefix all\nserialization group with the name of the entity. So instead of `admin` or \n`website` we did go with `article_admin` and `article_website`. \n\n```php\n/** @Groups({\"article_admin\", \"article_website\"}) */\nprivate string $title;\n```\n\nSo we don't have conflicts with related entities serialization groups.\nThis did work great for us and we avoided sideeffects well our APIs\nbut when looking at the serialization config ([xml config](https://jmsyst.com/libs/serializer/master/reference/xml_reference))\nor at the classes annotations it is hard to understand how the response\nof this object really looks without calling that endpoint.\n\n## New project new solutions\n\nIn a new project which was actually driven by Symfony UX with Symfony\nForms. I had the need that I needed to provide my entity as `array`\nto the form as we only used the form for validation and rendering\nthe form, but not actually to map data to the entity. As we did use\nCommandBus, CommandMessage and CommandHandlers via the Symfony Messenger\nto update our entities. And I wanted to avoid that we need to define\n`mapped =\u003e false` in all cases.\n\nSo the question was how do I convert my Entity into the `array` format\nof the form. First thought about using a Serializer but did find that\nis too much magic and wanted to have something more typesafe. So instead\nof a serializer I did go with a `toDetailFormArray` method on my entity.\n\n```php\nclass Article {\n    /**\n     * @return array{\n     *      title: string,\n     *      description: string|null,\n     * }\n     */\n    private function toDetailFormArray(): array\n    {\n        return [\n            'title' =\u003e $this-\u003etitle,\n            'description' =\u003e $this-\u003edescription,\n        ];\n    }\n} \n```\n\nThe entity is big and was splitted into several forms and each form did then have\nits own `toArray` method e.g.:\n\n```php\nclass Article {\n    /**\n     * @return array{\n     *      title: string,\n     *      description: string|null,\n     * }\n     */\n    private function toDetailFormArray(): array\n    {\n        // ...\n    }\n    \n    /**\n     * @return array{\n     *      keywords: string,\n     *      tags: string[],\n     * }\n     */\n    private function toSeoFormArray(): array\n    {\n        // ...\n    }\n} \n```\n\nThis is working really well and everybody is understanding and see the structure\nof the data we need here. Also testing this \"toArray\" is more efficient as it is\na Unit Test on the class. And also PHPStan is doing a great work here as we define\nthe return types of the array.\n\n## From Forms to API\n\nAs this did really feel good and did work well for my use cases I did more and more\nthink about it - should I not do the same when providing my entity via a JSON API.\n\nSo instead of having a serializer for my entity in the Controller I replace it with\na toArray method e.g.:\n\n```diff\n-$data = $this-\u003eserializer-\u003eserialize($article, ['some' =\u003e 'options']);\n+$data = $article-\u003etoAdminApiDetailArray();\n```\n\nAlso some APIs did require some required options example our Article is multi language\nand we want only provide a single language e.g. `/api/article/1?locale=en`.\n\nMostly this did work the following in our case we did adjust our entity the following way:\n\n```php\nclass Article {\n    private ?string $currentLocale;\n    \n    public function setCurrentLocale(string $currentLocale): void\n    {\n        $this-\u003ecurrentLocale = $currentLocale;\n    }\n    \n    public function getCurrentTranslation(): void\n    {\n        Assert::notNull($this-\u003ecurrentLocale, 'The \"currentLocale\" property is required to be set.');\n        \n        return $this-\u003egetTranslation($this-\u003ecurrentLocale);\n    }\n    \n    /**\n     * @throws ArticleTrnaslationNotFoundException\n     */\n    public function getTranslation(string $locale): ArticleTrnaslation {/* .. */}\n} \n```\n\nThe `currentLocale` needed to be set before serialization or by a more magic way via\na serialization listener.\n\n```php\n$article-\u003esetCurrentLocale($request-\u003equery-\u003egetAlnum('locale'));\n$data = $this-\u003eserializer-\u003eserialize($article, ['some' =\u003e 'options']);\n```\n\nInstead with an own method we can define required options to convert our entity into an array:\n\n```php\nclass Article {\n    /**\n     * @return array{\n     *      title: string,\n     *      description: string|null,\n     * }\n     */\n    public function toAdminApiDetailTranslatedArray(string $locale): array\n    {\n        $translation = $this-\u003egetTranslation($locale);\n    \n        return [\n            'id' =\u003e $translation-\u003egetTitle(),\n            'title' =\u003e $translation-\u003egetTitle(),\n            'description' =\u003e $translation-\u003egetDescription(),\n        ];\n    }\n}\n```\n\nThis is a lot better from my point of view for the developer experience. And does not hide\nanything behind some serializer subscriber or serializer options.\n\n## What about the Single Responsibility Principle\n\nThe [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)\ntells us that \"A class should have only one reason to change.\".\nI totally agree that it hurts the \"Single Responsibility Principle\". To fix that we could move\nthe whole method into an own Service called: `ArticleAdminApiDetailTranslatedFactory`.\n\n```php\nclass ArticleAdminApiDetailTranslatedFactoryInterface {\n    /**\n     * @return array{\n     *      title: string,\n     *      description: string|null,\n     * }\n     */\n    public function create(Article $article): array;\n}\n```\n\nThe kind of factories are used in many cases, mostly they are used to create an own Representation\nof an Object so not returning an array instead return again an own Model:\n\n```php\nclass ArticleAdminApiDetailTranslatedFactoryInterface {\n    public function create(Article $article): ArticleAdminApiDetailTranslatedRepresentation;\n}\n```\n\nInstead of a Factory we could directly go with an own Repository for this representation which fetches\nthe data by an array from the Article table:\n\n```php\nclass ArticleAdminApiDetailTranslatedRepository {\n    public function getOneBy(id): ArticleAdminApiDetailTranslatedRepresentation\n    {\n        $queryBuilder = $this-\u003eentityManager-\u003ecreateQueryBuilder()\n            -\u003eform(Article::class, 'article')\n            -\u003eselect('article.id')\n            -\u003eaddSelect('article.title')\n            -\u003eaddSelect('article.description');\n\n        $result = $queryBuilder-\u003egetSingleResult();\n\n        return new ArticleAdminApiDetailTranslatedRepresentation($result['id'], $result['title'], $result['description']);\n    }\n}\n```\n\nThat would be from DDD (Domain Driven Design) view mostly be the best solution.\n\n## Why I would go not with a Factory or own Repository\n\nWhy the above solution is very clean it is still something I would not use\nin my projects. The above solution would need a lot of more classes to be \nmaintained also to be tested. But the number one point what make for me\nthe above solution not a good one for my projects is extendability.\n\nExample in [Sulu CMS](https://github.com/sulu/sulu) we provide some\ncore entities/models. This models are [extendable](https://docs.sulu.io/en/2.4/cookbook/extend-entities.html).\nNow a developer wants an additional field for example on the Media table.\nFor this case they can extend from the exist model and add there new property:\n\n```php\n/**\n * @ORM\\Table(name=\"me_media\")\n * @ORM\\Entity\n */\nclass Media extends SuluMedia {   \n    /**\n     * @ORM\\Column(name=\"newProperty\", type=\"string\", length=255, nullable = true)\n     */\n    private string $newProperty;\n}\n```\n\nWith the previous solution for a factory with an own method or even a repository.\nWe would need a factory service for the representation class. This service\nwould the case of extendability be able to be overwritten so the end developer\nneed to extend the following things:\n\n 1. Model\n 2. Representation Model\n 3. Representation Model Factory\n\nSo the factory / repository solution is great about [SOLID principles](https://en.wikipedia.org/wiki/SOLID)\nbut not very developer friendly for extendability.\n\nWith the solution of to array is a lot faster to add a new property to a\nModel and its API by overwriting the `toArray` method e.g.:\n\n```php\n/**\n * @ORM\\Table(name=\"me_media\")\n * @ORM\\Entity\n */\nclass Media extends SuluMedia {   \n    /**\n     * @ORM\\Column(name=\"newProperty\", type=\"string\", length=255, nullable = true)\n     */\n    private string $newProperty;\n    \n    /**\n     * @return MediaAdminApiDetailTranslatedArray\u0026array{\n     *      newProperty: string|null,\n     * }\n     */\n    public function toAdminApiDetailTranslatedArray(string $locale): array\n    {\n        $data = parent::toAdminApiDetailTranslatedArray($locale);\n        $data['newProperty'] = $this-\u003enewProperty;\n    \n        return $data;\n    }\n}\n```\n\nSo instead of overwriting 3 classes only 1 class is required. This make it\nform my point of view a lot easier. So developers wanting to extend exist\nentities with their own properties to match there business logic.\n\nAs written a lot in last times - software should be created for humans\nand should not make human work more difficult.\n\n## API part of our Domain Logic\n\nAs I'm doing a lot of with [Hexagonal architecture](https://github.com/alexander-schranz/hexagonal-architecture-study)\nin the last time. The questions is that the API json structure should\nreally be part of the \"Application Core\". As it should maybe\nbe the API Controller \"Adapter\" defining its response structure\nand not the Domain Model. I'm thinking that the Structure of my\nAPI point are so important for my Business Logic that they should\nbe part of my Application Core and not should be defined outside\nof it over some Infrastructure configuration or other things. Alternate solution\nas listed above and [DDD](https://de.wikipedia.org/wiki/Domain-driven_Design)\nrepresentation models and repositories which I think hard sometimes\nto maintain and getting to whole Team into this Mindset.\n\n## Conclusion\n\nAt the end I think the toArray methods was I think in the past seen\nas little bit evil because they were not typesafe. But with tools like\n[phpstan](https://github.com/phpstan/phpstan) and [psalm](https://github.com/vimeo/psalm)\nyou can make sure that it will return also for arrays the correct types.\n\nI think the solution for toArray make this process:\n\n - understandable for beginners\n - more readable\n - easier to extend\n - easier to test\n - less dependencies / easier upgrade / easier maintainable\n\nBased on the toArray methods I found an easy wo to create an activity\nlog for our changes by making a diff on PHP internal methods before\nand after an entity was changed.\n\nI still can not say if in future I will build APIs this way at the end\nit always need to be a **Team Decision** what is the best way for a Team\nto develop things. What are they most familiar with and how they can\nprocess the fastest and maintainable way.\n\nAs written in the intro there are a cases were a serializer is still\nthe fastest and maintainable way to move forward. Still it didn't match\nmost of my cases or did make my cases a lot harder. I think libraries like\n[API Platform](https://github.com/api-platform/api-platform) are doing a\nreally great Job when you want to create a single endpoint for your entity.\nThe toolset around libraries like this is really great. And aslong as you are\nhappy with your solutions and your team is happy with it you should\nstay with things what works best for you.\n\n## Continue with a serializer\n\nWhat you can do when you continue with a serializer. In most cases\nto \"Test\" how an entity is serialized a whole API tests are created.\nInstead you should think about creating more abstract test cases\nwhere you just create the Model in the memory and call the serializer\non it. This will make your tests a lot faster then having the need\nto persist your Model to a database.\n\n```php\nclass MerchantSerializerTest extends TestCase {\n    public function testSerializeAdminGroup(): void\n    {\n        $article = new Article();\n        // .. call needed methods to create article\n        \n        $json = $this-\u003eserializer-\u003eserialize($article);\n\n        // ... assert json\n    }\n}\n```\n\nIn this case I also want to recommend the best way to test your serialization\nto `json` or `xml` is in my opinion the [Coduo PHP Matcher Library](https://github.com/coduo/php-matcher)\nThis way you can test your JSON or XML API against an json definition like this: \n\n```json\n{\n    \"id\": \"@integer@\",\n    \"title\": \"Title\",\n    \"description\": \"Title\"\n}\n```\n\nThe best is that this tests will work so with auto incremented ID's as you can define\nto match only the type of the response JSON. Also this way it is automatically\nrecognized if a new property is added to the response so automatically tests need\nto be adopted then and you don't forget about testing new properties. Sure the\nlibrary can also used to match against an array.\n\nI hope I could give at least one person some impact with this article. Let me know what\nyour solutions for API serialization are and what problems you did have in the past your\ndifferent solutions you tried.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexander-schranz%2Fdo-we-really-need-a-serializer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexander-schranz%2Fdo-we-really-need-a-serializer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexander-schranz%2Fdo-we-really-need-a-serializer/lists"}