ekanto/json-api-specifications
Installation Add the package via Composer:
composer require ekanto/json-api-specifications
Register the bundle in config/app.php (if using Symfony) or publish the config:
php artisan vendor:publish --provider="Ekanto\JASpecificationBundle\JASpecificationBundle"
First Use Case: Basic Resource Transformation
Define a resource class (e.g., PostResource):
namespace App\Resources;
use Ekanto\JASpecificationBundle\Resource\ResourceInterface;
use Ekanto\JASpecificationBundle\Resource\ResourceTrait;
use App\Models\Post;
class PostResource implements ResourceInterface
{
use ResourceTrait;
protected $model;
public function __construct(Post $post)
{
$this->model = $post;
}
public function getType()
{
return 'posts';
}
public function getId()
{
return $this->model->id;
}
public function getAttributes()
{
return [
'title' => $this->model->title,
'body' => $this->model->body,
];
}
}
Serialize a Resource
Use the ResourceSerializer to convert a resource to JSON:API format:
use Ekanto\JASpecificationBundle\Serializer\ResourceSerializer;
$serializer = new ResourceSerializer();
$postResource = new PostResource($post);
$jsonApiData = $serializer->serialize($postResource);
Resource Nesting (Relationships) Define relationships in your resource:
public function getRelationships()
{
return [
'author' => new AuthorResource($this->model->author),
];
}
Pagination
Use the Pagination class to handle pagination metadata:
use Ekanto\JASpecificationBundle\Serializer\Pagination;
$pagination = new Pagination($posts->total(), $posts->perPage(), $posts->currentPage());
$serializer->setPagination($pagination);
Customizing Serialization
Override default behavior by extending the ResourceSerializer:
class CustomSerializer extends ResourceSerializer
{
protected function getAttribute($resource, $key)
{
// Custom logic for specific attributes
if ($key === 'title') {
return strtoupper($resource->getAttributes()['title']);
}
return parent::getAttribute($key);
}
}
API Controller Integration Example controller method:
public function index()
{
$posts = Post::paginate(10);
$resources = $posts->map(fn ($post) => new PostResource($post));
$serializer = new ResourceSerializer();
$serializer->setPagination(new Pagination(...));
return response()->json($serializer->serializeCollection($resources));
}
Deserialization (Parsing JSON:API Requests)
Use ResourceDeserializer to parse incoming JSON:API requests:
use Ekanto\JASpecificationBundle\Serializer\ResourceDeserializer;
$deserializer = new ResourceDeserializer();
$data = $deserializer->deserialize($request->getContent(), PostResource::class);
Laravel HTTP Requests
Parse JSON:API requests directly from Laravel's Request object:
$data = $deserializer->deserialize($request->getContent());
Form Request Validation
Validate incoming JSON:API data using Laravel's FormRequest:
public function rules()
{
return [
'data.attributes.title' => 'required|string',
'data.attributes.body' => 'required|string',
];
}
API Versioning Combine with Laravel's API versioning middleware to support multiple JSON:API versions.
Testing
Use ResourceSerializer and ResourceDeserializer in PHPUnit tests to validate API responses/requests:
$serializer = new ResourceSerializer();
$json = $serializer->serialize($resource);
$this->assertJsonStringEqualsJson($expectedJson, $json);
Attribute Whitelisting
The serializer only includes attributes explicitly defined in getAttributes(). Omitting an attribute will not include it in the output, even if the model has it. Use getAdditionalAttributes() to include dynamic or all attributes:
public function getAdditionalAttributes()
{
return ['created_at', 'updated_at'];
}
Relationship Data Structure
Relationships must return a ResourceInterface or an array of ResourceInterface. Returning raw data (e.g., ['id' => 1]) will cause errors.
ID Field Naming
The serializer expects the ID field to be named id. If your model uses a different field (e.g., uuid), override getId():
public function getId()
{
return $this->model->uuid;
}
Pagination Metadata Ensure pagination metadata is set before serializing collections. Missing pagination will result in invalid JSON:API output.
Deserialization Edge Cases
setDataLoader on ResourceDeserializer to resolve nested resources:
$deserializer->setDataLoader(function ($type, $id) {
return Post::findOrFail($id);
});
Enable Debug Mode
Set the debug config option to true to get detailed error messages:
'debug' => env('APP_DEBUG', false),
Validate JSON:API Output Use tools like JSON:API Validator to validate your API responses.
Log Serialized Data
Log the output of ResourceSerializer to debug issues:
\Log::debug('Serialized JSON:API', ['data' => $serializer->serialize($resource)]);
Check for Deprecated Methods Monitor deprecation warnings in the package updates. Some methods may change between minor versions.
Custom Attribute Transformers
Extend ResourceSerializer to transform attributes dynamically:
protected function transformAttribute($resource, $key, $value)
{
if ($key === 'body') {
return Str::limit($value, 200);
}
return $value;
}
Custom Error Handling
Override error handling in ResourceDeserializer to customize validation errors:
protected function handleError($error)
{
throw new \InvalidArgumentException("Custom error: {$error->getMessage()}");
}
Adding Custom Metadata Extend the serializer to include custom metadata in the top-level object:
public function getMeta()
{
return [
'custom_field' => 'value',
];
}
Support for Polymorphic Relationships
Implement logic to handle polymorphic relationships (e.g., author could be a User or Guest):
public function getRelationships()
{
$author = $this->model->author;
$resource = $author instanceof User
? new UserResource($author)
: new GuestResource($author);
return ['author' => $resource];
}
Caching Serialized Responses Cache serialized resources to improve performance:
$cacheKey = "posts.{$post->id}";
$json = Cache::remember($cacheKey, now()->addHours(1), function () use ($serializer, $postResource) {
return $serializer->serialize($postResource);
});
How can I help you explore Laravel packages today?