Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Json Ld Api Bundle Laravel Package

dunglas/json-ld-api-bundle

A Symfony bundle for building JSON-LD and Hydra REST APIs from your entities. It adds schema.org vocabularies, pagination, filtering, validation, and API documentation, making it easy to expose hypermedia-driven web APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

First, reconcile the package name with reality. dunglas/json-ld-api-bundle is the historical name of the component that became API Platform Core; the dunglas/json-ld-api-bundle repository now redirects to api-platform/core. Nothing you install today should be called dunglas/json-ld-api-bundle — you install one of:

# Symfony application
composer require api-platform/core

# Laravel application
composer require api-platform/laravel

The old ApiPlatform\Core\* namespace is gone in v3; everything lives under ApiPlatform\Metadata, ApiPlatform\State, ApiPlatform\JsonLd, ApiPlatform\Hydra, ApiPlatform\Serializer. The current major is v5 (latest patch at time of writing: 5.0.4), so treat any snippet you find that targets ApiPlatform\Core\* as two majors stale and re-check the import path before pasting.

Minimal path to a first response:

  1. Register an ApiResource on the class you already have (Eloquent model in Laravel, Doctrine entity or plain class in Symfony).
  2. Declare operations explicitly — v3+ does not infer them from resource name.
  3. Ensure the routes are mounted (Laravel: publish config/api-platform.php and confirm the route prefix; Symfony: import the resource config).
  4. Call it with the correct Accept header.
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
    operations: [new Get(), new GetCollection()],
    paginationItemsPerPage: 20,
)]
class Product extends Model
{
    // ...
}
curl -H 'Accept: application/ld+json' https://app.test/api/products

You should get a Hydra envelope: @context, @id, @type, hydra:totalItems, hydra:member, hydra:view.

Where to look first, in order:

  • ApiPlatform\Metadata\ApiResource and the operation classes (Get, GetCollection, Post, Put, Patch, Delete) — this is the whole configuration surface now.
  • ApiPlatform\State\ProviderInterface / ProcessorInterface — the only extension points you should reach for on day one.
  • ApiPlatform\JsonLd\ContextBuilder and ApiPlatform\Hydra\Serializer\* — what actually shapes the payload.
  • /api/docs (Swagger UI / ReDoc) — fastest way to see whether your metadata is being read at all.

The first use case worth shipping: a read-only paginated collection with a filter. That single endpoint exercises content negotiation, IRI generation, pagination, and filtering — the four things that break most often.

Implementation Patterns

Mount it beside your existing app, don't rewrite. Treat API Platform as a resource layer inside Laravel/Symfony, not a replacement for controllers. Existing Blade/Inertia routes keep working; the resource layer owns /api.

Custom operations for anything non-CRUD. Don't fight the default CRUD; declare an operation with a provider (read) or processor (write):

#[ApiResource(operations: [
    new GetCollection(),
    new Get(uriTemplate: '/products/{id}'),
    new GetCollection(
        uriTemplate: '/products/popular',
        provider: PopularProductsProvider::class,
    ),
])]

State providers are the seam between API Platform and your domain. In Laravel these wrap Eloquent/query builders; the contract is tiny (return an object, an iterable, or a Paginator) and gives you full control over eager loading, scopes, and tenant filtering without touching serialization.

Output DTOs for anything that isn't a stable public shape. Declare output: ProductSummary::class on the operation and let a provider or processor assemble it. This decouples your database schema from your JSON-LD @type, which is exactly the contract you cannot change later.

Relations default to IRIs, not embedded objects. A belongsTo will emit "brand": "/api/brands/3". To embed, use #[ApiProperty(readableLink: true)] or serializer groups; to suppress entirely, #[Ignore]. Decide this per relation deliberately — it's the single biggest driver of client-side N+1 requests.

Put parameter configuration on the operation, not only on the property. #[ApiParameter] on a property is a default; an operation-level parameter overrides it (operation parameters take precedence over property attributes, and defaults that come from can* getters are applied consistently as of 5.0.4). If a filter or serialization setting "doesn't apply to one endpoint", look for a property-level ApiParameter shadowing your operation config.

Parameter operations: accepts class names now. Instead of instantiating operations inside a parameter/filter declaration, you can list [GetCollection::class, Get::class]. Prefer this for filters you want on reads only — it survives refactors better than object graphs, and GraphQL operation classes are accepted in parameter attributes on properties as well (5.0.4).

Validation happens in the processor. Wire a processor that runs Laravel's Validator (or Symfony's ValidatorInterface) and throws on failure; API Platform converts it to 422 with hydra:description and a violations array. Don't validate in a middleware — you lose operation-level context. For multi-step rules (e.g. "validate basic fields, then cross-field business rules"), use validator GroupSequences rather than hand-rolled ordering — supported since 5.0.4.

Auth is per-operation middleware. In Laravel, put middleware: ['auth:sanctum', 'throttle:api'] on write operations only. Reads usually stay public. There is no global "everything is protected" switch worth relying on.

Filters are opt-in and silent when missing. A ?name=Foo query string on a resource with no QueryParameter or SearchFilter is ignored, not rejected. That is a feature (the request still succeeds) and a debugging trap (it looks like the filter doesn't work).

Gotchas and Tips

406 is the default symptom. Send no Accept header from a plain HTTP client and you get 406 Not Acceptable, or the HTML docs page. Always pin Accept: application/ld+json (or application/hal+json / application/json for simpler consumers) in your tests and in your front-end fetch wrapper.

IRIs are a public API contract. @id values are generated from route names and identifiers. Renaming a route, changing the URL prefix, or switching integer IDs to UUIDs silently breaks every client that cached or stored an @id. Freeze the iri:/prefix config early and treat it as versioned.

JSON-LD is verbose and many clients won't resolve @context. If the consumer is a hand-written JS front-end rather than a linked-data client, HAL (application/hal+json) or plain JSON is often the pragmatic choice. You can expose several formats on the same resource without code changes — do that before you write a custom normalizer.

PATCH needs a specific content type. PATCH requires Content-Type: application/merge-patch+json; application/json will not merge. Semantics: null removes a property, omission leaves it untouched. Test both.

Metadata with*() methods are immutable. Put::withAllowCreate(), Link::withFromProperty(), and friends return a new instance rather than mutating in place (made consistent in 5.0.4). $operation->withAllowCreate(true); is a no-op — you must write $operation = $operation->withAllowCreate(true);. Same for the filter/provider precedence: an operation parameter's provider is no longer silently replaced by the filter provider, so if you were relying on that accident, set it explicitly.

422 is the answer for bad input, and the set of "bad input" grew. Empty scalar parameters and invalid backed-enum values both return 422 with a Hydra violation body. If your client or monitoring branched on 400 for those cases, update it; conversely, don't "fix" a 422 by loosening validation.

Empty arrays are a JSON-LD footgun. A relation that should be an object can serialize as [], which is invalid in @context-typed positions. Check the edge case where a relation is empty on one row only. In Laravel, an optional belongsTo that resolves to null is now accepted rather than blowing up — but verify the serialized shape either way.

Search filters and LIKE escaping are database-portable now. The escape character is chosen per platform instead of assumed, and nested-property joins in OrFilter/link relations use ON and left joins correctly. Two consequences: (a) re-run your partial-match/contains filter tests after upgrading — patterns that happened to work with a backslash-only escape may match differently; (b) if you build custom filter logic with raw LIKE, escape user-supplied % and _ yourself, matching the package's escape char so behaviour is consistent.

Metadata is cached. Attribute changes appear only after a cache clear; in Symfony, watch api_platform.metadata_cache and kernel.debug. A "my new operation is 404" report is almost always stale metadata or a missing cache clear.

Long-running workers need the patch-level resets. The skolem IRI converter (Symfony) and the Mercure publisher are now reset between requests (5.0.4). If you run under Octane, RoadRunner, FrankenPHP worker mode, or a Symfony runtime with a persistent container, upgrade within 5.0.x before debugging phantom leaks between requests.

Docs drift is a real failure mode. Several 5.0.4 fixes target the OpenAPI/JSON Schema output rather than runtime: nested resources now honour their output DTO, intersection types emit allOf, mixed emits an empty schema, operation groups survive into the context, and nested gen_id follows the property. If /api/docs disagreed with what the endpoint actually returned, that was a bug — upgrade before you write a custom schema factory.

Debugging checklist, in order: debug:router (Symfony) or route:list (Laravel) to confirm the route exists → /api/docs to confirm the operation was parsed → the Accept header → whether the resource metadata is cached → whether you discarded a with*() return value. Inspect metadata programmatically via ResourceMetadataCollectionFactoryInterface; don't guess.

Extension points, ranked by safety: custom Provider/Processor > SerializerContextBuilderInterface decorator (inject groups per operation) > decorating ItemNormalizer > forking ContextBuilder. Only the first two survive minor upgrades comfortably.

v2 → v3 vocabulary (and now v5). DataProvider → Provider, DataPersister → Processor, ApiResource annotations → attributes, and ApiPlatform\Core\* → ApiPlatform\*. Snippets you find online are overwhelmingly v2 and some are v3; verify the import path and the major version before pasting. Patch releases inside 5.0.x have been safe to take — clear the metadata cache after upgrading.

Testing: extend ApiPlatform\Test\ApiTestCase from the package itself (the import path was corrected in 5.0.4) — importing a stale copy from your own test helpers is how you end up testing a cached kernel. Assert on the Accept header and on @id stability, not just status codes.

If you touch the core repo itself (the AGENTS.md conventions apply): write a failing functional test first, run filtered tests only (vendor/bin/phpunit --filter testMethodName), never the full suite; create new fixtures rather than editing tests/Fixtures/; find src -name vendor -exec rm -rf {} + before a suite that hangs; and warm the cache with one test run before phpstan analyse src/YourComponent.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect