Skip to content

Add JsonAPI SchemaFactory support #3930

Description

@Martin-P

API Platform version(s) affected
2.5.9

Description
JSON:API methods POST, PUT and PATCH not working when using application/vnd.api+json. For comparison I also tested POST and PUT methods using application/ld+json which works perfectly.

PATCH is working when using application/merge-patch+json. Is this the correct behaviour or should PATCH also function correctly using application/vnd.api+json?

Info from the API Platform web interface:

POST
Request:

curl -X POST "http://api.api-platform.local/api/demos" -H  "accept: application/vnd.api+json" -H  "Content-Type: application/vnd.api+json" -d "{\"description\":\"TEST\",\"insertDate\":\"2021-01-03T19:52:15.163Z\",\"editDate\":\"2021-01-03T19:52:15.163Z\",\"deleteDate\":\"2021-01-03T19:52:15.163Z\",\"active\":true}"

Response:

Column 'description' cannot be null
{
  "title": "An error occurred",
  "description": "An exception occurred while executing 'INSERT INTO demo (description, insert_date, edit_date, delete_date, active) VALUES (?, ?, ?, ?, ?)' with params [null, \"2021-01-03 20:52:27\", null, null, 1]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'description' cannot be null",
  "trace": [
    {
      "namespace": "",
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\Driver\\AbstractMySQLDriver.php",
      "line": 125,
      "args": []
    },
    {
      "namespace": "Doctrine\\DBAL\\Driver",
      "short_class": "AbstractMySQLDriver",
      "class": "Doctrine\\DBAL\\Driver\\AbstractMySQLDriver",
      "type": "->",
      "function": "convertException",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\DBALException.php",
      "line": 182,
      "args": [
        [
          "string",
          "An exception occurred while executing 'INSERT INTO demo (description, insert_date, edit_date, delete_date, active) VALUES (?, ?, ?, ?, ?)' with params [null, \"2021-01-03 20:52:27\", null, null, 1]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'description' cannot be null"
        ],
        [
          "object",
          "Doctrine\\DBAL\\Driver\\PDO\\Exception"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\DBAL",
      "short_class": "DBALException",
      "class": "Doctrine\\DBAL\\DBALException",
      "type": "::",
      "function": "wrapException",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\DBALException.php",
      "line": 159,
      "args": [
        [
          "object",
          "Doctrine\\DBAL\\Driver\\PDO\\MySQL\\Driver"
        ],
        [
          "object",
          "Doctrine\\DBAL\\Driver\\PDO\\Exception"
        ],
        [
          "string",
          "An exception occurred while executing 'INSERT INTO demo (description, insert_date, edit_date, delete_date, active) VALUES (?, ?, ?, ?, ?)' with params [null, \"2021-01-03 20:52:27\", null, null, 1]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'description' cannot be null"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\DBAL",
      "short_class": "DBALException",
      "class": "Doctrine\\DBAL\\DBALException",
      "type": "::",
      "function": "driverExceptionDuringQuery",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\Connection.php",
      "line": 2121,
      "args": [
        [
          "object",
          "Doctrine\\DBAL\\Driver\\PDO\\MySQL\\Driver"
        ],
        [
          "object",
          "Doctrine\\DBAL\\Driver\\PDO\\Exception"
        ],
        [
          "string",
          "INSERT INTO demo (description, insert_date, edit_date, delete_date, active) VALUES (?, ?, ?, ?, ?)"
        ],
        [
          "array",
          {
            "1": [
              "null",
              null
            ],
            "2": [
              "string",
              "2021-01-03 20:52:27"
            ],
            "3": [
              "null",
              null
            ],
            "4": [
              "null",
              null
            ],
            "5": [
              "integer",
              1
            ]
          }
        ]
      ]
    },
    {
      "namespace": "Doctrine\\DBAL",
      "short_class": "Connection",
      "class": "Doctrine\\DBAL\\Connection",
      "type": "->",
      "function": "handleExceptionDuringQuery",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\Statement.php",
      "line": 173,
      "args": [
        [
          "object",
          "Doctrine\\DBAL\\Driver\\PDO\\Exception"
        ],
        [
          "string",
          "INSERT INTO demo (description, insert_date, edit_date, delete_date, active) VALUES (?, ?, ?, ?, ?)"
        ],
        [
          "array",
          {
            "1": [
              "null",
              null
            ],
            "2": [
              "object",
              "DateTime"
            ],
            "3": [
              "null",
              null
            ],
            "4": [
              "null",
              null
            ],
            "5": [
              "boolean",
              true
            ]
          }
        ],
        [
          "array",
          {
            "1": [
              "string",
              "string"
            ],
            "2": [
              "string",
              "datetime"
            ],
            "3": [
              "string",
              "datetime"
            ],
            "4": [
              "string",
              "datetime"
            ],
            "5": [
              "string",
              "boolean"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "Doctrine\\DBAL",
      "short_class": "Statement",
      "class": "Doctrine\\DBAL\\Statement",
      "type": "->",
      "function": "execute",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\orm\\lib\\Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister.php",
      "line": 286,
      "args": []
    },
    {
      "namespace": "Doctrine\\ORM\\Persisters\\Entity",
      "short_class": "BasicEntityPersister",
      "class": "Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister",
      "type": "->",
      "function": "executeInserts",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\orm\\lib\\Doctrine\\ORM\\UnitOfWork.php",
      "line": 1097,
      "args": []
    },
    {
      "namespace": "Doctrine\\ORM",
      "short_class": "UnitOfWork",
      "class": "Doctrine\\ORM\\UnitOfWork",
      "type": "->",
      "function": "executeInserts",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\orm\\lib\\Doctrine\\ORM\\UnitOfWork.php",
      "line": 402,
      "args": [
        [
          "object",
          "Doctrine\\ORM\\Mapping\\ClassMetadata"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\ORM",
      "short_class": "UnitOfWork",
      "class": "Doctrine\\ORM\\UnitOfWork",
      "type": "->",
      "function": "commit",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\doctrine\\orm\\lib\\Doctrine\\ORM\\EntityManager.php",
      "line": 371,
      "args": [
        [
          "null",
          null
        ]
      ]
    },
    {
      "namespace": "Doctrine\\ORM",
      "short_class": "EntityManager",
      "class": "Doctrine\\ORM\\EntityManager",
      "type": "->",
      "function": "flush",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\var\\cache\\dev\\ContainerRKJeEJJ\\EntityManager_9a5be93.php",
      "line": 129,
      "args": [
        [
          "null",
          null
        ]
      ]
    },
    {
      "namespace": "ContainerRKJeEJJ",
      "short_class": "EntityManager_9a5be93",
      "class": "ContainerRKJeEJJ\\EntityManager_9a5be93",
      "type": "->",
      "function": "flush",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\api-platform\\core\\src\\Bridge\\Doctrine\\Common\\DataPersister.php",
      "line": 60,
      "args": []
    },
    {
      "namespace": "ApiPlatform\\Core\\Bridge\\Doctrine\\Common",
      "short_class": "DataPersister",
      "class": "ApiPlatform\\Core\\Bridge\\Doctrine\\Common\\DataPersister",
      "type": "->",
      "function": "persist",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\api-platform\\core\\src\\Bridge\\Symfony\\Bundle\\DataPersister\\TraceableChainDataPersister.php",
      "line": 56,
      "args": [
        [
          "object",
          "App\\Entity\\Demo"
        ],
        [
          "array",
          {
            "resource_class": [
              "string",
              "App\\Entity\\Demo"
            ],
            "collection_operation_name": [
              "string",
              "post"
            ],
            "receive": [
              "boolean",
              true
            ],
            "respond": [
              "boolean",
              true
            ],
            "persist": [
              "boolean",
              true
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Bridge\\Symfony\\Bundle\\DataPersister",
      "short_class": "TraceableChainDataPersister",
      "class": "ApiPlatform\\Core\\Bridge\\Symfony\\Bundle\\DataPersister\\TraceableChainDataPersister",
      "type": "->",
      "function": "persist",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\api-platform\\core\\src\\EventListener\\WriteListener.php",
      "line": 76,
      "args": [
        [
          "object",
          "App\\Entity\\Demo"
        ],
        [
          "array",
          {
            "resource_class": [
              "string",
              "App\\Entity\\Demo"
            ],
            "collection_operation_name": [
              "string",
              "post"
            ],
            "receive": [
              "boolean",
              true
            ],
            "respond": [
              "boolean",
              true
            ],
            "persist": [
              "boolean",
              true
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\EventListener",
      "short_class": "WriteListener",
      "class": "ApiPlatform\\Core\\EventListener\\WriteListener",
      "type": "->",
      "function": "onKernelView",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\symfony\\event-dispatcher\\Debug\\WrappedListener.php",
      "line": 117,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "WrappedListener",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener",
      "type": "->",
      "function": "__invoke",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\symfony\\event-dispatcher\\EventDispatcher.php",
      "line": 230,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "callListeners",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\symfony\\event-dispatcher\\EventDispatcher.php",
      "line": 59,
      "args": [
        [
          "array",
          [
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ]
          ]
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\symfony\\event-dispatcher\\Debug\\TraceableEventDispatcher.php",
      "line": 151,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "TraceableEventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\symfony\\http-kernel\\HttpKernel.php",
      "line": 162,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handleRaw",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\symfony\\http-kernel\\HttpKernel.php",
      "line": 79,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handle",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\project-api-platform\\vendor\\symfony\\http-kernel\\Kernel.php",
      "line": 195,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ],
        [
          "boolean",
          true
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "Kernel",
      "class": "Symfony\\Component\\HttpKernel\\Kernel",
      "type": "->",
      "function": "handle",
      "file": "C:\\Data\\Webdesign\\domains\\api-platform.local\\git\\public_html\\index.php",
      "line": 20,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ]
      ]
    }
  ]
}

PUT
Request:

curl -X PUT "http://api.api-platform.local/api/demos/2" -H  "accept: application/vnd.api+json" -H  "Content-Type: application/vnd.api+json" -d "{\"description\":\"Something using PUT\",\"insertDate\":\"2021-01-03T19:50:18.586Z\",\"editDate\":\"2021-01-03T19:50:18.586Z\",\"deleteDate\":\"2021-01-03T19:50:18.586Z\",\"active\":true}"

Response:

The old information (no replace happened)
{
  "data": {
    "id": "/api/demos/2",
    "type": "Demo",
    "attributes": {
      "demoId": 2,
      "description": "Patch test 3rd time",
      "insertDate": "2021-01-03T19:35:30+01:00",
      "editDate": "2021-01-03T20:49:25+01:00",
      "active": true
    }
  }
}

How to reproduce
config/packages/api_platform.yaml

api_platform:
    mapping:
        paths: ['%kernel.project_dir%/src/Entity']

    swagger:
        versions: [3]

    # The list of enabled formats. The first one will be the default.
    formats:
        jsonapi:
            mime_types: ['application/vnd.api+json']
        jsonld:
            mime_types: ['application/ld+json']
        json:
            mime_types: ['application/json']
        html:
            mime_types: ['text/html']

    # By default, API Platform supports JSON Merge Patch and JSON:API PATCH formats.
    # Support for the JSON:API PATCH format is automatically enabled if JSON:API
    # support is enabled. JSON Merge Patch support must be enabled explicitly:
    patch_formats:
        json: ['application/merge-patch+json']
        jsonapi: ['application/vnd.api+json']
        
    error_formats:
        jsonproblem: ['application/problem+json']
        jsonld: ['application/ld+json']      # Hydra error formats
        jsonapi: ['application/vnd.api+json']

App\Entity\Demo.php

<?php declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use DateTime;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Description of Demo
 * 
 * @ApiResource
 * 
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 * @ORM\Table(name="demo")
 */
class Demo
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer", name="demo_id")
     */
    private $demoId;

    /**
     * @var string
     *
     * @ORM\Column(type="string", length=255, nullable=false)
     */
    private $description;

    /**
     * @ORM\Column(type="datetime")
     */
    private $insertDate;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $editDate;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $deleteDate;

    /**
     * @ORM\Column(type="boolean")
     */
    private $active = true;

    /**
     * @ORM\PrePersist
     */
    public function setInsertDateValue()
    {
        $this->insertDate = new DateTime();
    }

    /**
     * @ORM\PreUpdate
     */
    public function setEditDateValue()
    {
        $this->editDate = new DateTime();
    }

    public function getDemoId()
    {
        return $this->demoId;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getInsertDate(): ?DateTimeInterface
    {
        return $this->insertDate;
    }

    public function setInsertDate(DateTimeInterface $insertDate): self
    {
        $this->insertDate = $insertDate;

        return $this;
    }

    public function getEditDate(): ?DateTimeInterface
    {
        return $this->editDate;
    }

    public function setEditDate(?DateTimeInterface $editDate): self
    {
        $this->editDate = $editDate;

        return $this;
    }

    public function getDeleteDate(): ?DateTimeInterface
    {
        return $this->deleteDate;
    }

    public function setDeleteDate(?DateTimeInterface $deleteDate): self
    {
        $this->deleteDate = $deleteDate;

        return $this;
    }

    public function getActive(): ?bool
    {
        return $this->active;
    }

    public function setActive(bool $active): self
    {
        $this->active = $active;

        return $this;
    }
}

Possible Solution
Not a solution, but looking at the stack trace it seems the data does not reach the API platform DataPersister.

Additional Context

  • PHP 7.4.13
  • Symfony 5.2.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions