HalJsonResponseAdapter
Unwrap HAL+JSON <https://stateless.group/hal_specification.html>_ response envelopes for ApiBackend.
Overview
HAL (Hypertext Application Language) is a widely-adopted convention for embedding hypermedia controls in JSON APIs. A typical HAL list response looks like this:
{
"_links": {"self": {"href": "/articles"}},
"_embedded": {
"articles": [
{
"id": 1,
"title": "Hello World",
"_links": {"self": {"href": "/articles/1"}}
}
]
}
}
Without an adapter, ApiBackend has no way to know that the real records are buried inside _embedded.articles. HalJsonResponseAdapter navigates that nesting automatically and delivers a clean list of plain dicts to the model’s column-mapping layer:
[{"id": 1, "title": "Hello World"}]
Supported envelope shapes
The adapter handles a broad range of real-world payload shapes in priority order:
- Idiomatic HAL —
_embeddedwrapping a list or a named sub-resource. - Named list containers — top-level keys such as
"items","results","data"(see :attr:list_container_keys). - Bare list — a JSON array at the root is returned as-is.
- Key→record map — a dict whose non-metadata values are all dicts is treated as a named record collection; the key is injected as
"key"into each record. - Recursive fallback — any nested value that contains a list is unwrapped.
For single-record extraction the adapter checks :attr:record_container_keys first, then _embedded, and finally returns the entire response dict as a last resort (useful for APIs that return a bare object with no wrapper key).
Normalisation
:meth:_normalize_record removes keys listed in :attr:metadata_keys ("_links" by default) and promotes _embedded sub-resources from each record to the top level. Existing top-level keys are never overwritten by embedded content.
Note that _links is only stripped from individual record objects inside the list, not from the top-level response envelope. ApiBackend drives pagination from HTTP response headers (the Link header and X-Total-Count), so the top-level _links body field is never needed by the framework. If you subclass ApiBackend and override get_next_page_data_from_response to read _links from the JSON body, that data is still available on the raw response.json() call — the adapter does not touch it.
Full working example
Define a backend subclass that points at the upstream HAL service and attach the adapter, then wire it into a model and expose it through a list endpoint:
import clearskies
class ArticleBackend(clearskies.backends.ApiBackend):
def __init__(self):
super().__init__(
base_url="https://hal.example.com",
response_adapter=clearskies.backends.adapters.HalJsonResponseAdapter(),
)
class Article(clearskies.Model):
id_column_name = "id"
backend = ArticleBackend()
id = clearskies.columns.Integer()
title = clearskies.columns.String()
wsgi = clearskies.contexts.WsgiRef(
clearskies.endpoints.List(
model_class=Article,
readable_column_names=["id", "title"],
sortable_column_names=["id"],
default_sort_column_name=None,
default_limit=10,
),
classes=[Article],
)
wsgi()
Assuming https://hal.example.com/articles returns a HAL envelope, the endpoint unwraps it transparently:
$ curl 'http://localhost:8080/' | jq
{
"status": "success",
"error": "",
"data": [
{"id": 1, "title": "Hello"},
{"id": 2, "title": "World"}
],
"pagination": {"number_results": 2, "limit": 10, "next_page": {}},
"input_errors": {}
}
Customising the adapter
All lookup keys are defined as class attributes so that subclasses can extend or replace them without overriding any logic:
from clearskies.backends.adapters import HalJsonResponseAdapter
class MyHalAdapter(HalJsonResponseAdapter):
# Add "entries" as an additional list container key.
list_container_keys = HalJsonResponseAdapter.list_container_keys + ("entries",)
# Also strip "_meta" from each normalised record.
metadata_keys = HalJsonResponseAdapter.metadata_keys + ("_meta",)
Binding the adapter via DI
You can register the adapter in the dependency injection container instead of setting it on the backend instance:
context = clearskies.contexts.WsgiRef(
clearskies.endpoints.List(model_class=Article, ...),
bindings={"response_adapter": HalJsonResponseAdapter()},
)
An adapter set directly on the backend always takes precedence over one registered via DI.