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

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

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

  [ home ](https://www.msaied.com)    [ articles ](https://www.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%2Fwww.msaied.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%2Fwww.msaied.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://www.msaied.com/articles) 

 [ ![MySQL Query Profiling in Laravel: EXPLAIN ANALYZE, Slow Query Log, and Index Hints](https://cdn.msaied.com/481/8bfc417cb94b3e2868428e065fe4b166.png) laravel mysql performance 

### MySQL Query Profiling in Laravel: EXPLAIN ANALYZE, Slow Query Log, and Index Hints

Stop guessing why a query is slow. Learn how to use EXPLAIN ANALYZE, Laravel's slow query log listener, and in...

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

 28 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/mysql-query-profiling-in-laravel-explain-analyze-slow-query-log-and-index-hints) [ ![Laravel Custom Eloquent Relations: Building Polymorphic and Composite-Key Relations](https://cdn.msaied.com/480/c7d2c0d0fd1823460fbdb138f77b573f.png) laravel eloquent database 

### Laravel Custom Eloquent Relations: Building Polymorphic and Composite-Key Relations

Eloquent's built-in relations cover 90% of cases, but composite-key joins and non-standard polymorphic pivots...

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

 28 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-custom-eloquent-relations-building-polymorphic-and-composite-key-relations) [ ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Overhead](https://cdn.msaied.com/479/d7c54bd7a42ee57610a29f19a2c5e0fa.png) laravel postgresql jsonb 

### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Overhead

JSONB columns unlock flexible schemas, but misused they become performance sinkholes. This guide covers GIN in...

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

 28 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-overhead) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.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://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.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)
