AxelG's avatar
Level 1

Polymorphic resources in new JSON:API resources

I am trying to use the new JSON:API resource feature added in Laravel 13, but I don't see anything in the doc related to polymorphic relations.
The doc about this feature is in the Eloquent section, and I understand that it is built on top on Eloquent, so why is there no mention of polymorphic relations? And more importantly, how can I make it work?

Suppose I have an Inventory model that has an items relationship to an Item class which is polymorphic through the itemable relationship. The itemable can either return a Consumable or Serialized object. How should I define the toRelationships on the ItemResource class to return either a ConsumableResource or a SerializedResource?

I found this solution for the moment:

public function toRelationships(Request $request)
{
    $itemableResourceClass = "App\\Http\\Resources\\" . class_basename(Relation::getMorphedModel($this->itemable_type)) . "Resource";
    return [
        'itemable' => $itemableResourceClass,
    ];
}

But it doesn't work with nested includes like GET /inventories?include=items.itemable.reference.
I get the correct itemable resource, but the reference relationship is not included in the resource.

Also it is not very clean.

1 like
5 replies
imranbru's avatar
imranbru
Best Answer
Level 4

The reason your nested inclusion items.itemable.reference is failing is that the JSON:API manager needs the explicit resource class to traverse the inclusion tree. Dynamic string concatenation often prevents the manager from correctly mapping the subsequent reference relationship during the eager-loading phase.

The cleanest way to handle this is using a match expression or a mapping array to return the class constants directly. This ensures the inclusion context is preserved.

In your ItemResource, define the relationship like this:

public function toRelationships(Request $request): array
{
    return [
        'itemable' => match ($this->itemable_type) {
            'consumable' => ConsumableResource::class,
            'serialized' => SerializedResource::class,
            default => null,
        },
    ];
}

By returning the ::class constant, Laravel's JSON:API engine can immediately identify the target resource. It then looks at the toRelationships method of the resolved resource (e.g., ConsumableResource) to find the reference definition.

If you have many polymorphic types, you can refactor this into a protected property or a dedicated resolver method to keep it tidy:

protected array $itemableMap = [
    'consumable' => ConsumableResource::class,
    'serialized' => SerializedResource::class,
];

public function toRelationships(Request $request): array
{
    return [
        'itemable' => $this->itemableMap[$this->itemable_type] ?? null,
    ];
}

This approach is much more robust than string manipulation and fully supports nested includes like ?include=items.itemable.reference.

AxelG's avatar
Level 1

Thanks for your answer!

I tried your solution, however when I call POST inventories/1/items?include=itemable.reference (which was my actual test case) and dump the response content, I only see the itemable relationship.
From some other tests I made, I think this is normal because nested relationships are only shown in the included section of the response. However, what is not normal is that the response is missing the included part...

I am calling my route in a test, here is the relevant part:

$response = $this->postJson(
    route('inventories.items.store', [
        'inventory' => $inventory->id,
        'include' => 'itemable.reference',
    ]),
    $data,
);
dump($response->json());

The dump gives me

array:1 [
  0 => array:4 [
    "id" => "21"
    "type" => "items"
    "attributes" => array:3 [
      "id" => 21
      "itemable_id" => 21
      "itemable_type" => "consumable"
    ]
    "relationships" => array:1 [
      "itemable" => array:1 [
        "data" => array:2 [
          "id" => "21"
          "type" => "consumables"
        ]
      ]
    ]
  ]
]

And here is my controller function:

public function store(Inventory $inventory, StoreItemRequest $request, ItemService $itemService)
{
    $items = $itemService->hydrateItems($request->items);

    $items = $itemService->addOrStackItems($inventory, $items);
    return response($items->toResourceCollection(), 201);
}

The $items variable being cast as a ResourceCollection is a Collection of Item models.

Do you see anything that I'm doing wrong?

imranbru's avatar

The reason the included section is missing is that manual resource conversion via toResourceCollection() doesn't automatically parse the include query parameter from the request. When you call it manually in the controller, you're responsible for passing that inclusion tree to the resource.

You can fix this by chaining the withIncludes method:

public function store(Inventory $inventory, StoreItemRequest $request, ItemService $itemService)
{
    $items = $itemService->hydrateItems($request->items);
    $items = $itemService->addOrStackItems($inventory, $items);

    return $items->toResourceCollection()
        ->withIncludes(explode(',', $request->query('include', '')))
        ->response()
        ->setStatusCode(201);
}

Two things to keep in mind:

  1. Eager Loading: Ensure your ItemService (specifically addOrStackItems) is eager-loading the itemable and itemable.reference relationships. The JSON:API serializer generally won't include data that isn't already loaded on the models to avoid N+1 query issues.
  2. Explicit Mapping: Since you're using polymorphic relations, the withIncludes call relies on the match expression you wrote in toRelationships to know exactly which resource class to use for the nested reference inclusion. If that mapping is correct, the included key will now populate.
AxelG's avatar
Level 1

I'm a bit confused, the doc doesn't mention that you have to manually set the includes, and the withIncludes() method you mentioned doesn't appear anywhere... I also didn't find anything about this function on google. Can you point me to the source?

AxelG's avatar
Level 1

Ok, I figured it out. Your first answer do work, I was having troubles because of the way I made the call.

For some reason, the additional parameter I was passing to the route function was included in the body of the request instead of the query, unlike the documentation says.
It was working fine with a get request, but not a post. I appended manually the parameters at the end of the url and it works like a charm.

Thanks for your help!

Please or to participate in this conversation.