Example: JSON and raw body
POST /api/v1/orders takes a JSON order checked by a schema. PUT /api/v1/products/import takes CSV as the raw body.
| Body | Response |
|---|---|
| a valid order | 201 {"customer":"Ada","lines":2,"pieces":5} |
| none | 400, missing data |
{"customer": | 400 Syntax error |
quantity: 0 or an unknown field | 400 with the schema errors |
CSV with the header sku,price | 200 {"imported":2,"products":[...]} |
| an empty PUT | 400 Field is required |
| CSV with another header | 422 from the handler |
The schema
json
{
"type": "object",
"properties": {
"customer": {"type": "string", "minLength": 1},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1}
},
"required": ["sku", "quantity"],
"additionalProperties": false
}
},
"note": {"type": "string"}
},
"required": ["customer", "items"],
"additionalProperties": false
}The JSON handler
$params['order'] is the decoded array; the handler runs only when the schema passed.
php
<?php
declare(strict_types=1);
namespace App\Api;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\JsonInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;
final class OrderCreateHandler extends BaseHandler
{
public function summary(): string
{
return 'Create an order from a JSON body';
}
public function params(): array
{
return [
// the whole request body, decoded to an array and checked against the schema
(new JsonInputParam('order', (string) file_get_contents(__DIR__ . '/schema/order.json')))
->setRequired()
->addExample('default', ['customer' => 'Ada', 'items' => [['sku' => 'book', 'quantity' => 2]]]),
];
}
public function handle(array $params): ResponseInterface
{
// already valid here: customer is a string, every item has sku and quantity >= 1
$order = $params['order'];
$pieces = array_sum(array_column($order['items'], 'quantity'));
return new JsonApiResponse(201, [
'customer' => $order['customer'],
'lines' => count($order['items']),
'pieces' => $pieces,
]);
}
}The raw body handler
php
<?php
declare(strict_types=1);
namespace App\Api;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\RawInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;
final class CsvImportHandler extends BaseHandler
{
public function summary(): string
{
return 'Import products from a CSV body';
}
public function params(): array
{
return [
// the body as a string, nette-api only checks that it is not empty
(new RawInputParam('csv'))->setRequired()->addExample('default', "sku,price\nbook,12.5\npen,1.2"),
];
}
public function handle(array $params): ResponseInterface
{
$lines = array_values(array_filter(array_map('trim', explode("\n", $params['csv']))));
$header = str_getcsv(array_shift($lines), escape: '');
if ($header !== ['sku', 'price']) {
// the format is checked here, in the handler, and answered with your own code
return new JsonApiResponse(422, ['status' => 'error', 'message' => 'The first line must be sku,price']);
}
$products = [];
foreach ($lines as $line) {
[$sku, $price] = str_getcsv($line, escape: '') + [1 => '0'];
$products[] = ['sku' => $sku, 'price' => (float) $price];
}
return new JsonApiResponse(200, ['imported' => count($products), 'products' => $products]);
}
}The services
yaml
application:
mapping:
Api: Tomaj\NetteApi\Presenters\*Presenter
services:
router: App\RouterFactory::create
apiPresenter:
factory: Tomaj\NetteApi\Presenters\ApiPresenter
- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
- Tomaj\NetteApi\Error\DefaultErrorHandler
apiDecider:
factory: Tomaj\NetteApi\ApiDecider
setup:
# POST /api/v1/orders with a JSON body
- addApi(Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'orders'), App\Api\OrderCreateHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())
# PUT /api/v1/products/import with a CSV body
- addApi(Tomaj\NetteApi\EndpointIdentifier('PUT', 1, 'products', 'import'), App\Api\CsvImportHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())The test
bash
#!/usr/bin/env bash
# End-to-end check: a JSON body validated by a schema, and a raw CSV body parsed by the handler.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f vendor/autoload.php ]] || { echo "fail: vendor/ is missing, run composer install first" >&2; exit 1; }
source ../ci/lib.sh
serve 8091
JSON=(-H 'Content-Type: application/json')
request -X POST "${JSON[@]}" -d '{"customer":"Ada","items":[{"sku":"book","quantity":2},{"sku":"pen","quantity":3}]}' "$BASE/api/v1/orders"
expect_status 201 "valid order"
expect_body '{"customer":"Ada","lines":2,"pieces":5}' "valid order"
ok "a valid JSON body reaches the handler as an array"
request -X POST "${JSON[@]}" "$BASE/api/v1/orders"
expect_status 400 "empty body"
expect_body '"detail":{"order":["missing data"]}' "empty body"
ok "an empty body answers 400 missing data"
request -X POST "${JSON[@]}" -d '{"customer":' "$BASE/api/v1/orders"
expect_status 400 "broken JSON"
expect_body '"order":["Syntax error"]' "broken JSON"
ok "broken JSON answers 400 with the json_decode error"
request -X POST "${JSON[@]}" -d '{"customer":"Ada","items":[{"sku":"book","quantity":0}]}' "$BASE/api/v1/orders"
expect_status 400 "schema"
expect_body 'quantity' "schema"
ok "a body that breaks the schema answers 400 with the failing field"
request -X POST "${JSON[@]}" -d '{"customer":"Ada","items":[{"sku":"book","quantity":1}],"coupon":"FREE"}' "$BASE/api/v1/orders"
expect_status 400 "additional property"
ok "an unknown property answers 400 because of additionalProperties: false"
request -X PUT -H 'Content-Type: text/csv' --data-binary $'sku,price\nbook,12.5\npen,1.2\n' "$BASE/api/v1/products/import"
expect_status 200 "csv"
expect_body '{"imported":2,"products":[{"sku":"book","price":12.5},{"sku":"pen","price":1.2}]}' "csv"
ok "a CSV body arrives as a string and the handler parses it"
request -X PUT -H 'Content-Type: text/csv' --data-binary $'name,cost\nbook,1\n' "$BASE/api/v1/products/import"
expect_status 422 "wrong csv header"
ok "the handler answers 422 for a CSV with the wrong header"
request -X PUT "$BASE/api/v1/products/import"
expect_status 400 "empty csv"
expect_body '"csv":["Field is required"]' "empty csv"
ok "an empty raw body answers 400"FAQ
Does the client need Content-Type: application/json?
No, JsonInputParam reads php://input whatever the header says. Send it anyway, the console and OpenAPI do.
Can one endpoint have a JSON body and query params?
Yes, add GetInputParam next to the JsonInputParam. Only one body param per endpoint makes sense, the body is read once.
How do I reject unknown fields?
"additionalProperties": false in the schema, as in order.json. See Input params.
Where do I check things a schema cannot?
In the handler, like the CSV header check that answers 422. Every code you return needs an output, see Outputs.
Is the schema shown in OpenAPI?
Yes, the JSON schema of a JsonInputParam becomes the request body of the endpoint. See OpenAPI.