core
core copied to clipboard
Handling multipart form arrays
Description
A multipart/form-data POST fails when an array property is included, even when it's correctly sent via Curl as follows:
-F 'tags=["One","Two","Three"];type=application/json'
Example
When using OpenAPI docs to upload a file via the API and including an array property, this fails as the generated Curl contains this:
-F 'tags=One,Two,Three'
The exception message is:
> "detail": "The type of the \u0022tags\u0022 attribute must be \u0022array\u0022, \u0022string\u0022 given"
> "file" "vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php","line":526,"function":"createForUnexpectedDataType"
> "class":"Symfony\\Component\\Serializer\\Exception\\NotNormalizableValueException"
Manually changing the Curl output on the command line to:
-F 'tags=[One,Two,Three]'
also fails with the same exception.
What works is:
-F 'tags=["One","Two","Three"]'
However, to get OpenApi to generate the above requires setting the contentType value of the property to application/json in the encoding part of the request body's content - as per https://github.com/swagger-api/swagger-ui/issues/4826#issuecomment-2160653633
The full Post definition is like so:
new Post(
inputFormats: ['multipart' => ['multipart/form-data']],
validationContext: ['groups' => ['create']],
openapi: new Model\Operation(
requestBody: new Model\RequestBody(
content: new \ArrayObject([
'multipart/form-data' => [
'schema' => [
'type' => 'object',
'properties' => [
'file' => [
'type' => 'string',
'format' => 'binary',
],
'tags' => [
'type' => 'array',
'items' => [
'type' => 'string',
'example' => '"One"',
],
'example' => ['One, 'Two', 'Three'],
],
],
],
'encoding' => [
'tags' => [
'contentType' => 'application/json',
],
],
],
])
)
)
This then results in the following generated Curl from the OpenApi docs:
-F 'tags=["One","Two","Three"];type=application/json'
According to the OpenApi specification 3.0 under "Media Type Object", encoding is
"A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded.
So this should work.
The issue is that when API Platform handles a POST - using MultipartDecoder - the tags property is not in $request->request->all() but in $request->files->all(). This is because tags is in $_FILES - so this is most likely a PHP parsing issue.
Workaround solution
Make tags a string:
'tags' => [
'type' => 'string',
'example' => 'One,Two,Three',
],
then manually explode the comma-separate string into an array in MultipartDecoder:
public function decode(string $data, string $format, array $context = []): ?array
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
return null;
}
$post = $request->request->all();
$value = array_map(static function (string $element) {
// Multipart form values will be encoded in JSON.
$decoded = json_decode($element, true);
return \is_array($decoded) ? $decoded : $element;
}, $post) + $request->files->all();
if (!is_array($post['tags']) && array_key_exists('tags', $post)) {
$value['tags'] = explode(',', $post['tags']);
}
return $value;
}
The curl post is:
-F 'tags=One,Two,Three'
Conclusion
I'm not sure if this should be a bug report (but it's an underlying issue with PHP), or a feature request (OpenAPI already has an upstream solution but PHP doesn't support it), or a documentation suggestion (to add this example to the existing docs)?
Related: https://github.com/api-platform/core/issues/4557
You need a custom normalizer API Platform doesn't support this format by default, also you're decoder is definitely the right solution for this issue, maybe that we can document that?
I don't think we want this feature as then we open the door to too many use cases.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.