Laravel API Resources: Sparse Fieldsets &amp; Contracts | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Contracts        On this page       1. [  Beyond toArray: Treating Resources as API Contracts ](#beyond-codetoarraycode-treating-resources-as-api-contracts)
2. [  Sparse Fieldsets Without a Package ](#sparse-fieldsets-without-a-package)
3. [  Conditional Relationships Without N+1 ](#conditional-relationships-without-n1)
4. [  Versioning Resources Without Duplication ](#versioning-resources-without-duplication)
5. [  Enforcing the Contract in Tests ](#enforcing-the-contract-in-tests)
6. [  Takeaways ](#takeaways)

  ![Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Contracts](https://cdn.msaied.com/476/2fe247e6705cd8624395e9194ff80703.png)

  #laravel   #api   #eloquent   #rest  

 Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Contracts 
==========================================================================================

     27 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Beyond toArray: Treating Resources as API Contracts  ](#beyond-codetoarraycode-treating-resources-as-api-contracts)
2. [  02   Sparse Fieldsets Without a Package  ](#sparse-fieldsets-without-a-package)
3. [  03   Conditional Relationships Without N+1  ](#conditional-relationships-without-n1)
4. [  04   Versioning Resources Without Duplication  ](#versioning-resources-without-duplication)
5. [  05   Enforcing the Contract in Tests  ](#enforcing-the-contract-in-tests)
6. [  06   Takeaways  ](#takeaways)

 Beyond `toArray`: Treating Resources as API Contracts
-----------------------------------------------------

Most Laravel codebases use `JsonResource` as a glorified `toArray` call. That works until a mobile client starts requesting only three fields, a second API version ships, or a relationship accidentally exposes internal pricing data. Resources are your last line of defence before JSON hits the wire — treat them accordingly.

---

Sparse Fieldsets Without a Package
----------------------------------

JSON:API specifies `?fields[articles]=title,body` to limit response payload. You can implement a lightweight version natively.

```php
// app/Http/Resources/Concerns/SparseFieldset.php
trait SparseFieldset
{
    protected function sparse(array $fields): array
    {
        $requested = request()->query('fields');

        if (blank($requested)) {
            return $fields;
        }

        $allowed = array_flip(
            array_map('trim', explode(',', $requested))
        );

        return array_intersect_key($fields, $allowed);
    }
}

```

```php
// app/Http/Resources/ArticleResource.php
class ArticleResource extends JsonResource
{
    use SparseFieldset;

    public function toArray(Request $request): array
    {
        return $this->sparse([
            'id'         => $this->id,
            'title'      => $this->title,
            'body'       => $this->body,
            'created_at' => $this->created_at->toIso8601String(),
        ]);
    }
}

```

A request to `GET /articles/1?fields=id,title` returns only those two keys. No extra package, no middleware — just a trait.

> **Security note:** never expose fields that are not explicitly listed in the resource. The whitelist is the contract; the query string is only a filter on top of it.

---

Conditional Relationships Without N+1
-------------------------------------

`whenLoaded` is well-known, but the pattern breaks down when you forget to eager-load in the controller. Pair it with a resource collection that enforces the load:

```php
// app/Http/Resources/ArticleCollection.php
class ArticleCollection extends ResourceCollection
{
    public static $wrap = 'data';

    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total' => $this->resource->total(),
                'per_page' => $this->resource->perPage(),
            ],
        ];
    }
}

```

```php
// ArticleController
public function index(): ArticleCollection
{
    $articles = Article::query()
        ->with(['author', 'tags'])
        ->paginate(25);

    return new ArticleCollection($articles);
}

```

Inside `ArticleResource`:

```php
public function toArray(Request $request): array
{
    return $this->sparse([
        'id'     => $this->id,
        'title'  => $this->title,
        'author' => new AuthorResource($this->whenLoaded('author')),
        'tags'   => TagResource::collection($this->whenLoaded('tags')),
    ]);
}

```

`whenLoaded` returns `MissingValue` when the relation is absent, which Eloquent silently omits from the JSON. The relationship key disappears entirely rather than serialising as `null` — a meaningful distinction for API consumers.

---

Versioning Resources Without Duplication
----------------------------------------

Avoid copying entire resource classes per version. Extend and override only what changed:

```php
// app/Http/Resources/V2/ArticleResource.php
namespace App\Http\Resources\V2;

use App\Http\Resources\ArticleResource as V1ArticleResource;

class ArticleResource extends V1ArticleResource
{
    public function toArray(Request $request): array
    {
        return array_merge(parent::toArray($request), [
            'slug'    => $this->slug,        // new in v2
            'excerpt' => $this->excerpt,     // new in v2
            'body'    => $this->when(
                $request->boolean('include_body'),
                $this->body
            ),
        ]);
    }
}

```

Route groups resolve the correct namespace:

```php
Route::prefix('v2')->namespace('App\Http\Controllers\V2')->group(
    base_path('routes/api_v2.php')
);

```

---

Enforcing the Contract in Tests
-------------------------------

Use Pest's `assertJson` structure assertions to lock the shape:

```php
it('returns stable article structure', function () {
    $article = Article::factory()->for(User::factory(), 'author')->create();

    $this->getJson("/api/v1/articles/{$article->id}")
        ->assertOk()
        ->assertJsonStructure([
            'data' => ['id', 'title', 'body', 'created_at'],
        ])
        ->assertJsonMissingPath('data.author'); // not loaded, must be absent
});

```

This catches accidental field additions or relationship leaks before they reach production.

---

Takeaways
---------

- Implement sparse fieldsets with a simple trait — no JSON:API package required.
- `whenLoaded` omits keys entirely when relations are absent; use that intentionally.
- Version resources by extension, not duplication — override only what changed.
- Write structure-assertion tests to lock your API contract and catch regressions early.
- Resources are a security boundary: whitelist fields explicitly, never pass `$this->resource->toArray()` blindly.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-api-resources-sparse-fieldsets-conditional-relationships-and-stable-contracts&text=Laravel+API+Resources%3A+Sparse+Fieldsets%2C+Conditional+Relationships%2C+and+Stable+Contracts) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-api-resources-sparse-fieldsets-conditional-relationships-and-stable-contracts) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does `whenLoaded` return `null` or omit the key when the relation is not loaded?        It returns a `MissingValue` instance, which Laravel's JSON serialisation silently drops. The key is omitted from the response entirely, not serialised as `null`. This is intentional and useful for distinguishing 'not requested' from 'explicitly null'. 

      Q02  How do I prevent sparse fieldsets from exposing sensitive fields a client should never see?        The `sparse()` trait only filters down from the whitelist you define in `toArray`. A client can request fewer fields but never more than what the resource explicitly declares. Sensitive fields simply should not appear in the resource's field map. 

      Q03  Is extending a V1 resource for V2 safe when V1 changes?        It depends on your change policy. If V1 is frozen (common after a stable release), extension is safe. If V1 is still evolving, consider an abstract base resource that both versions extend, keeping shared logic in one place without coupling the versions directly. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Eloquent Query Optimization: Killing N+1 Problems at the Source](https://cdn.msaied.com/475/39c5bf23764003ebc9ac32d840752d49.png) laravel eloquent performance 

### Eloquent Query Optimization: Killing N+1 Problems at the Source

N+1 queries silently destroy Laravel app performance. This guide shows senior engineers how to detect, elimina...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 27 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/eloquent-query-optimization-killing-n1-problems-at-the-source) [ ![Laravel API Rate-Limiting: Custom Limiters, Per-Route Strategies, and Header Contracts](https://cdn.msaied.com/474/47b5edd1bda5625b01ae20f7684ed199.png) laravel api rate-limiting 

### Laravel API Rate-Limiting: Custom Limiters, Per-Route Strategies, and Header Contracts

Go beyond the default throttle middleware. Build named rate limiters with dynamic keys, per-user tiers, and co...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 27 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-api-rate-limiting-custom-limiters-per-route-strategies-and-header-contracts-1) [ ![Profiling Laravel with Blackfire and Xdebug: Finding Real Bottlenecks](https://cdn.msaied.com/473/5b2261450fb7c3c68870997e338c2e1b.png) laravel performance profiling 

### Profiling Laravel with Blackfire and Xdebug: Finding Real Bottlenecks

Stop guessing where your Laravel app is slow. Learn how to use Blackfire and Xdebug profilers together to pinp...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/profiling-laravel-with-blackfire-and-xdebug-finding-real-bottlenecks-1) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
