Summary
An engineering team encountered a significant architectural hurdle while attempting to implement a Hypermedia as the Engine of Application State (HATEOAS) compliant API using Spring HATEOAS. The requirement was to return a single resource that contained both metadata (custom properties) and a collection of embedded resources within the _embedded object, as defined by the HAL (Hypertext Application Language) specification. The team struggled because the standard CollectionModel is designed to wrap a list, whereas the desired output required a composite structure where metadata and collections coexist at the same level of the JSON hierarchy.
Root Cause
The core issue stems from a misunderstanding of the inheritance model and serialization behavior of Spring HATEOAS wrapper classes:
- Single Responsibility Principle of Wrappers:
EntityModel<T>is architected to wrap a single domain object, whileCollectionModel<T>is architected to wrap a collection. Neither is designed to act as a “container” for both arbitrary properties and a collection simultaneously. - Standard JSON Serialization: When extending
RepresentationModelto add custom fields (likecurrentlyProcessing), standard Jackson serialization treats any nested object (including aCollectionModel) as a regular field. This results in the collection being nested under its variable name (e.g.,"myOrders": { ... }) rather than being moved into the spec-compliant_embeddedblock. - The HAL Specification Gap: The HAL spec requires collections to reside in
_embedded. Spring’s built-in models enforce this via specific internal logic that is not easily triggered when the models are nested as properties within a custom class.
Why This Happens in Real Systems
In complex distributed systems, we often move beyond simple CRUD operations. We encounter “Dashboard” or “Summary” endpoints where:
- Aggregated State Matters: You need to return global statistics (e.g.,
total_count,system_status) alongside the actual data items. - Strict Spec Compliance: Third-party consumers or frontend frameworks (like those using HAL-driven state machines) expect a very specific JSON shape.
- Model Rigidity: Frameworks like Spring HATEOAS provide excellent abstractions for the 90% use case, but their strict adherence to specific patterns makes the “edge case” of a metadata-rich collection wrapper difficult to implement without fighting the framework.
Real-World Impact
- Breaking API Contracts: If developers attempt to “force” the structure by simply nesting models, the resulting JSON violates the HAL specification, breaking client-side hypermedia parsers.
- Developer Friction: Engineers spend excessive time creating “wrapper for a wrapper” classes (the “Boilerplate Explosion”), which increases the cognitive load and maintenance cost of the codebase.
- Payload Inconsistency: Inconsistent JSON structures across different endpoints make it difficult to write generic client-side interceptors or data mappers.
Example or Code
To achieve the desired HAL-compliant structure, one must create a custom class that extends RepresentationModel and manually handles the _embedded population or uses a specific DTO pattern.
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
public class OrderSummaryModel extends RepresentationModel {
private final long currentlyProcessing;
private final long shippedToday;
private final CollectionModel orders;
public OrderSummaryModel(long currentlyProcessing, long shippedToday, CollectionModel orders) {
this.currentlyProcessing = currentlyProcessing;
this.shippedToday = shippedToday;
this.orders = orders;
}
public long getCurrentlyProcessing() {
return currentlyProcessing;
}
public long getShippedToday() {
return shippedToday;
}
// The key is to ensure the Jackson serializer treats 'orders'
// as part of the _embedded block or to use a custom DTO.
public CollectionModel getOrders() {
return orders;
}
}
Note: In a production implementation, one would typically use a custom Jackson Serializer or a dedicated DTO that maps the CollectionModel properties into a Map that Jackson flattens into _embedded.
How Senior Engineers Fix It
A senior engineer looks past the immediate “class inheritance” problem and addresses the serialization strategy:
- Composition over Inheritance: Instead of trying to make
CollectionModelbehave differently, we design a Summary DTO that explicitly defines the metadata and provides a way to inject the collection into the_embeddedmap. - Custom Serializers: For high-scale systems, we implement a custom Jackson
JsonSerializerfor our summary models. This ensures that the_embeddedkey is populated correctly according to the HAL spec without requiring deep inheritance hierarchies. - Mapping Layers: We decouple the Domain Model from the Representation Model. We use a mapping layer (like MapStruct) to transform a complex
OrderSummarydomain object into a HAL-compliantRepresentationModelwhere the_embeddedfield is manually populated viaaddEmbedded(collection).
Why Juniors Miss It
- Fighting the Framework: Juniors often try to solve the problem by extending existing classes (e.g.,
extends CollectionModel) and adding fields, not realizing that the internal mechanics of the parent class will prevent the desired JSON shape. - Focusing on Syntax vs. Spec: They focus on “How do I make this variable appear in this JSON key?” rather than “How does the HAL specification define the relationship between metadata and embedded resources?”
- Boilerplate Overload: They often fall into the trap of creating deep hierarchies of “Wrapper” classes to satisfy a single endpoint, failing to see that the issue is a serialization mismatch rather than a missing class.