Laravel API Resources: Sparse Fieldsets &amp; Contracts | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com/public) - [Home](https://msaied.com/public)
- [Projects](https://msaied.com/public/projects)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://msaied.com/public/certificates)
- [About](https://msaied.com/public#about)

           [  Contact](https://msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com/public)
- [ProjectsCase studies](https://msaied.com/public/projects)
- [ArticlesEngineering notes](https://msaied.com/public/articles)
- [CertificatesCredentials](https://msaied.com/public/certificates)
- [AboutHow I work](https://msaied.com/public#about)
- [ContactGet in touch](https://msaied.com/public#contact)

  [Start a conversation](https://msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com/public)
2. /
3. [Articles](https://msaied.com/public/articles)
4. /
5. Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Contracts

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

 Go beyond basic transformers. Learn how to implement sparse fieldsets, conditionally load relationships, and enforce stable API contracts using Laravel's Eloquent API Resources — without leaking domain internals.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com/public#person) Published 27 Jul 2026 · Updated 27 Jul 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

- [laravel](https://msaied.com/public/articles?search=laravel)
- [api](https://msaied.com/public/articles?search=api)
- [eloquent](https://msaied.com/public/articles?search=eloquent)
- [rest](https://msaied.com/public/articles?search=rest)

 Frequently asked questions 
---------------------------

  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'.

   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.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleEloquent Query Optimization: Killing N+1 Problems at the Source](https://msaied.com/public/articles/eloquent-query-optimization-killing-n1-problems-at-the-source) [Next articleLaravel Performance: HTTP Response Caching, Cache Tags, and Stale-While-Revalidate](https://msaied.com/public/articles/laravel-performance-http-response-caching-cache-tags-and-stale-while-revalidate)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/745/8744e1be5136b430da52e9fca3ed3964.png)  · 3 min read### Service Container Deep Dive: Contextual Binding, Tagging, and Method Injection

6 Oct 2026 ](https://msaied.com/public/articles/service-container-deep-dive-contextual-binding-tagging-and-method-injection-1) [ ![](https://cdn.msaied.com/743/8998fac3a41451ab3fe1588194e17a43.png) Filament · 3 min read### Securing Filament Plugins with Plumb: Automated Security Scoring for PHP Packages

5 Oct 2026 ](https://msaied.com/public/articles/securing-filament-plugins-with-plumb-automated-security-scoring-for-php-packages) [ ![](https://cdn.msaied.com/742/2d02018669cdeedccb5de2efb898f0ee.png) Filament · 3 min read### Filament v3.3.56 Released: File Hash Names and Livewire Upload Fix

5 Oct 2026 ](https://msaied.com/public/articles/filament-v3356-released-file-hash-names-and-livewire-upload-fix) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com/public)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/public/sitemap.xml)
