Skip to main content
Version: 2.0-rc

Hypermedia & HAL

Model ManagementModel ManagementThe part of the platform that defines, versions and stores the data models and integration configuration (the model artifacts) from which the platform is configured. It is implemented by Model Forge, an embedded module of the Portal Backend. responses are hypermedia-enabled: a single resource tells a client where to go next. Instead of hard-coding URL templates, a client reads the _links object on a response and follows the relation it needs — self, schema, dependencies, source, target, the next page, and so on.

The wire format is HAL (Hypertext Application Language): the JSON object every client already parses, plus a reserved _links member for navigation.

This is additive

HAL is layered on top of the existing JSON representation. The entity's own fields stay exactly where they were, at the top level; _links simply appears beside them. A client that does not care about hypermedia ignores the underscore-prefixed members and keeps working unchanged.

What HATEOAS and HAL are​

HATEOAS (Hypermedia As The Engine Of Application State) is the REST principle that a response carries the links describing what a client can do next, so the client navigates by following links rather than by assembling URLs from out-of-band knowledge.

HAL is a small, widely supported convention for expressing those links in JSON. A HAL document is an ordinary JSON object with two optional reserved members:

  • _links — a map of link relation → link object. Each link object carries an href (and templated for URI templates). The relation name is the map key.
  • _embedded — a map of relation → full embedded resource(s), for inlining related data when a round-trip would be wasteful. Model ManagementModel ManagementThe part of the platform that defines, versions and stores the data models and integration configuration (the model artifacts) from which the platform is configured. It is implemented by Model Forge, an embedded module of the Portal Backend. does not emit _embedded; when a client wants related data inlined, that is the job of /resolve.
{
"id": "urn:core:platform:civitas:mapping:common:sensor-to-observation:1.0.0",
"title": "Sensor to Observation",
"source": "urn:core:platform:civitas:datastructure:common:Sensor:1.0.0",
"target": "urn:core:platform:civitas:datastructure:common:Observation:1.0.0",
"_links": {
"self": { "href": "/api/v1/mappings?id=urn%3Acore%3A…%3Asensor-to-observation%3A1.0.0" },
"collection": { "href": "/api/v1/mappings" },
"source": { "href": "/api/v1/datastructures?id=urn%3Acore%3A…%3ASensor%3A1.0.0" },
"target": { "href": "/api/v1/datastructures?id=urn%3Acore%3A…%3AObservation%3A1.0.0" }
}
}

Why Model Management uses it​

The API is graph-shaped: DataStructures depend on DataStructures, mappings join a source and a target, pipelines wire sources, sinks and mappings together, datasets aggregate all of them. HAL lets the server hand the client those edges directly, which:

  • removes URL-template duplication in clients — the frontend no longer rebuilds …/dependencies?id= or …/datastructures?id=<source> strings itself;
  • encodes identities correctly — CORE URNsCORE URNThe global identity Model Management assigns to every model artifact: a unique, stable and versionable URN that exists independently of storage location, file names or registry technology. contain colons and travel in the ?id= query parameter; the server already URL-encodes them into every href, so clients never have to;
  • makes the API discoverable — start at one URL and follow links (see Entry point).

The entry point​

GET /api/v1 returns a links-only HAL document — the one URL a client needs to know. From here every top-level collection and cross-cutting endpoint is one link away:

{
"_links": {
"self": { "href": "/api/v1" },
"datastructures": { "href": "/api/v1/datastructures" },
"datasets": { "href": "/api/v1/datasets" },
"mappings": { "href": "/api/v1/mappings" },
"pipelines": { "href": "/api/v1/pipelines" },
"datasources": { "href": "/api/v1/datasources" },
"datasinks": { "href": "/api/v1/datasinks" },
"core-ir": { "href": "/api/v1/core-ir/meta-schemas" },
"resolve": { "href": "/api/v1/datastructures/resolve{?ids,include,depth}", "templated": true },
"xrepository": { "href": "/api/v1/xrepository/search{?q,page,size}", "templated": true },
"released": { "href": "/api/v1/artifacts/released{?id}", "templated": true }
}
}

The entry point lives under /api/v1, so — like every other /api/v1/* endpoint — it requires the X-API-Key header. The released and diagnostics links are conditional: released appears only when the (opt-in) stage workflow is enabled via MODELFORGE_WORKFLOW_ENABLED=true, and diagnostics only when the (opt-in) diagnostics endpoint is enabled via MODELFORGE_DIAGNOSTICS_ENABLED=true, so the entry point never advertises a relation that would resolve to 404.

Hypermedia is attached to the navigational read surface: every single-resource GET and every paged-collection GET. Write responses (POST/PUT/PATCH) already point onward through their Location header and standard status codes, so they are not wrapped.

Single resources​

ResourceGETRelations in _links
DataStructure…/datastructures?id=self, collection, schema, envelope, dependencies, dependents, impact, transitive-dependencies, maps-to, mapped-from, versions, dereferenced, bundled, json-schema
DataStructure envelope…/datastructures/envelope?id=self, entity, collection, dependencies, dependents, maps-to, mapped-from, versions
Mapping…/mappings?id=self, collection, and source/target (each a DataStructure link, when set)
Pipeline…/pipelines?id=self, collection, and datasources/datasinks/mappings — one link per referenced resource
DataSource / DataSink…/datasources?id= · …/datasinks?id=self, collection
DataSet…/datasets?id=self, collection, and datastructures/datasources/datasinks/mappings/pipelines — one link per member reference

A relation whose underlying reference is absent is omitted (a mapping with no target has no target link; a pipeline with no sinks has no datasinks relation).

Collections​

Every paged list (PagedResponse<T>) carries pagination links derived from the served page:

RelationPresent when
selfalways — the current page
firstalways — page=0
lastalways — the final page
prevthe current page is not the first
nextthe current page is not the last
itemalways — a templated link, …{?id}, for fetching a single member
{
"items": ["urn:core:…:A:1.0.0", "urn:core:…:B:1.0.0"],
"total": 137,
"page": 0,
"size": 50,
"_links": {
"self": { "href": "/api/v1/datastructures?page=0&size=50" },
"first": { "href": "/api/v1/datastructures?page=0&size=50" },
"last": { "href": "/api/v1/datastructures?page=2&size=50" },
"next": { "href": "/api/v1/datastructures?page=1&size=50" },
"item": { "href": "/api/v1/datastructures{?id}", "templated": true }
}
}

Content negotiation​

The HAL body is returned regardless of the requested media type, so the _links are always available. The Content-Type is negotiated:

  • default (Accept: application/json, */*, or no header) → application/json. Existing clients see no change beyond the extra _links member.
  • opt-in (Accept: application/hal+json) → application/hal+json, the media type that explicitly signals a HAL document.
# plain JSON (with _links)
curl -H "X-API-Key: $KEY" "http://localhost:8000/api/v1/mappings?id=$URN"

# signalled HAL
curl -H "X-API-Key: $KEY" -H "Accept: application/hal+json" \
"http://localhost:8000/api/v1/mappings?id=$URN"

Relationship to /resolve ($expand)​

HAL _links answer “where is the related resource?” with a one-line reference. When a client instead wants the related data inlined in a single round-trip — HAL's _embedded territory, and the equivalent of OData $expand or JSON:API include — that is what GET /api/v1/datastructures/resolve already provides: it resolves many URNs at once with configurable include projections (schema, bundled, dereferenced, dependencies, transitive). The two compose: navigate by _links, expand by resolve.

Design notes​

The implementation follows the project's minimalism (and the same reasoning that kept heavyweight, non-Spring-native frameworks out elsewhere):

  • Hand-rolled, not Spring HATEOAS. The published client library (model-forge-api) is deliberately Spring-free. The HAL primitives — a Link record and a generic HalResource<T> wrapper — are plain Java records in that module, so library consumers (including non-Spring ones) can read HAL responses with nothing but Jackson on the classpath. Spring HATEOAS would have pulled Spring types into that library.
  • Inline rendering via a Jackson 3 serializer. HalResource<T> renders the wrapped entity's fields at the top level and then writes _links. This is done by resolving the serializer for the content's concrete runtime type and using its unwrapping variant — which sidesteps the type erasure that makes a @JsonUnwrapped generic field unreliable.
  • Root-relative hrefs. Links are /api/v1/…, matching the Location headers the API already emits; no host resolution is involved.
  • Custom relation names. Domain relations (dependencies, source, datasources, …) use plain, readable rel names rather than CURIEs, mapping one-to-one to the endpoints documented in the artifact relations graph concept.