Delivering only the fields a client actually needs through a generic fields parameter
Most REST resources carry far more fields than a given client actually needs, and nested resources make that worse by pulling in entire related objects. A generic ?fields= parameter with dot-notation support solves this once, centrally, in the serializer, instead of being reimplemented for every endpoint.
Table of Contents
- 1. Why REST endpoints often return more than is needed
- 2. The ?fields= query parameter pattern in practice
- 3. Implementing it as a generic serializer filter in Symfony
- 4. Nested field selection in detail
- 5. Performance gains on large, nested resources
- 6. How this differs from GraphQL field selection
- 7. Why a generic filter beats endpoint-specific solutions
- 8. Pitfalls around caching and field validation
- 9. When sparse fieldsets are worth it
- 10. Summary
- 11. FAQ
1. Why REST endpoints often return more than is needed
A typical REST resource such as a product or a customer carries noticeably more fields in practice than a single client actually needs: description text, metadata, nested relations to other resources, internal timestamps. A mobile list view often only needs id, name, and price, yet the server returns the full object with every relation by default, because the endpoint offers no way to narrow the response down.
The problem compounds with nested resources: if an order automatically pulls in the full customer including addresses and payment methods, the response size quickly grows to a multiple of what the calling UI actually renders. For mobile clients on constrained bandwidth, or list endpoints returning hundreds of objects at once, this overhead adds up to noticeably slower load times and unnecessary data usage.
2. The ?fields= query parameter pattern in practice
The established solution is a query parameter, usually called fields, through which the client explicitly states which fields it wants in the response: GET /products/42?fields=id,name,price then returns only those three fields instead of the full product object. If the parameter is omitted, the endpoint still returns its full default response, so existing clients keep working unchanged, a decisive point for backward compatibility.
This pattern is commonly known in the REST community as sparse fieldsets or partial response, popularized among others by the JSON:API specification and Google's earlier partial response conventions. The key design principle is that field selection should work generically across arbitrary resources, rather than being defined separately for each endpoint.
3. Implementing it as a generic serializer filter in Symfony
Rather than manually checking which fields were requested in every controller and hand-assembling the response, a central solution at the serializer level pays off. A dedicated normalizer reads the fields parameter from the request context and recursively filters the already-normalized array before it is emitted as JSON. That keeps the filtering logic in exactly one place and it works automatically for every resource that goes through the same serializer.
Supporting dot notation for nested fields matters here, so that ?fields=id,customer.name,customer.email can select not just top-level fields but specific fields within a nested resource. The normalizer below first groups the requested fields by their root key and then applies itself recursively to nested arrays.
<?php
declare(strict_types=1);
final class SparseFieldsetNormalizer implements NormalizerInterface
{
public function __construct(private readonly ObjectNormalizer $decorated)
{
}
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
$data = $this->decorated->normalize($object, $format, $context);
if (!isset($context['fields']) || !is_array($context['fields'])) {
return $data;
}
return $this->filterFields($data, $context['fields']);
}
/**
* Recursively filters by the requested fields, including dot notation
* for nested resources (e.g. "customer.name").
*/
private function filterFields(array $data, array $fields): array
{
$grouped = [];
foreach ($fields as $field) {
[$root, $rest] = array_pad(explode('.', $field, 2), 2, null);
$grouped[$root][] = $rest;
}
$result = [];
foreach ($grouped as $key => $nestedFields) {
if (!array_key_exists($key, $data)) {
continue;
}
$value = $data[$key];
$nestedFields = array_filter($nestedFields);
$result[$key] = ($nestedFields !== [] && is_array($value))
? $this->filterFields($value, $nestedFields)
: $value;
}
return $result;
}
public function supportsNormalization(mixed $data, ?string $format = null): bool
{
return $this->decorated->supportsNormalization($data, $format);
}
}
4. Nested field selection in detail
Dot notation allows drilling down deliberately without the server having to expose a separate endpoint for every possible resource and sub-resource combination. ?fields=id,name,customer.name,customer.email,items.sku, for example, returns an order object with only the listed top-level fields, the customer name and email within customer, and only the sku within each item.
Technically this means the filter has to build a tree structure while parsing the query parameter, where each dot marks another level of nesting. If a requested sub-field is entirely missing from the tree for a given parent field, that parent field is dropped by default rather than accidentally returning with its full content, which would otherwise undermine the whole point of field selection.
5. Performance gains on large, nested resources
The payload benefit is usually marginal for flat, small resources, but grows quickly significant for deeply nested objects with many relations. An order list with fifty entries, where every entry automatically pulls in the customer, addresses, and every item with full product data, can easily reach ten or twenty times the size a plain overview list actually needs.
The performance gain is not limited to raw transfer size: if field selection is threaded consistently down into the data access layer rather than filtered only at the serializer level, unnecessary database joins and lazy-loading hits on relations that were never requested can be avoided too. Filtering purely at the serializer level saves transfer size but not necessarily database load, an important distinction for the architecture decision.
6. How this differs from GraphQL field selection
GraphQL solves the same underlying problem in a structurally different way: field selection is a native part of the query language itself and gets validated against a strict type system, invalid field names produce a clear error at query time instead of being silently ignored. GraphQL also allows selecting nested fields to arbitrary depth with per-level arguments, something a simple dot notation in REST can only approximate.
The price for that power is extra infrastructure: a GraphQL schema, per-field resolvers, and usually a separate tooling ecosystem. Sparse fieldsets in REST are a deliberately smaller, pragmatic solution for teams that want to stay on their existing REST architecture while still addressing most of the payload problem, without switching to a whole different API architecture.
7. Why a generic filter beats endpoint-specific solutions
A common anti-pattern is implementing field selection directly in every controller, for instance with if-statements that strip individual fields from the response array depending on the query parameter. That works for a single endpoint but quickly leads to inconsistent behavior across endpoints and duplicates the same logic in many places in the codebase.
A central normalizer or event listener that interprets the fields parameter uniformly across all endpoints instead delivers consistent behavior throughout the API and only needs to be tested and maintained in one place. New endpoints get sparse fieldset support automatically, without developers having to remember to implement it again for every new controller.
8. Pitfalls around caching and field validation
If HTTP caching is used for endpoints with sparse fieldsets, the fields parameter absolutely must be part of the cache key, otherwise the cache could incorrectly return a previously cached full response for a request with ?fields=id,name, or the other way around. A Vary header alone is not enough here, since Vary applies to request headers, not query parameters, so the cache key needs to be explicitly extended with the fields value.
Just as important is a whitelist of valid field names: without validation, a client could inadvertently request internal fields through field selection that were never meant to be exposed by the API, fields that only exist on the object for internal purposes. A robust implementation therefore checks requested fields against an explicitly allowed field list per resource and ignores or reports unknown fields instead of passing them through unchecked.
9. When sparse fieldsets are worth it
Sparse fieldsets pay off mainly where resources carry many fields or deep nesting and different clients (mobile, web, third-party) need very different slices of the data. For small, flat resources with few fields, the implementation effort often outweighs the actual benefit. The table below compares the key criteria for the decision.
| Criterion | Without Sparse Fieldsets | With Sparse Fieldsets | Recommendation |
|---|---|---|---|
| Payload size on large resources | Always complete | Only requested fields | Sparse fieldsets for wide objects |
| Implementation effort per endpoint | None (default serialization) | Solved centrally (filter/normalizer) | Generic filter instead of one-off solutions |
| Caching complexity | Simple (fixed payload) | Higher (cache key must include fields) | Extend Vary header/cache key |
| Client flexibility | None | High (client picks fields) | Sparse fieldsets for heterogeneous clients |
| Comparison to GraphQL | n/a | Similar goal, less powerful | GraphQL for complex query requirements |
Mironsoft
OpenAPI design, Symfony APIs, and API security
APIs that external teams can integrate without back-and-forth questions?
We review existing REST APIs for inconsistent error formats, missing OpenAPI documentation, and security gaps, then build an API that is clearly documented, versioned, and hardened against abuse.
API Review
Checking the OpenAPI spec, error formats, and status codes for consistency.
Symfony Implementation
Using DTOs, Serializer, and Validator for clean, type-safe request/response models.
Security Audit
Hardening rate limiting, auth schemes, and input validation against real attack surfaces.
10. Summary
Sparse Fieldsets: The Essentials at a Glance
Core problem
Default REST responses often carry noticeably more fields and relations than a given client actually needs.
The fix
A generic fields parameter with dot notation allows targeted field selection, including within nested resources.
Architecture
A central serializer filter, rather than endpoint-specific solutions, keeps behavior consistent across the whole API.
Practical advice
Extend the cache key with the fields parameter and validate requested fields against a whitelist to avoid cache bugs and data leaks.