feat(jsonapi): JsonResource[T] generic base with auto-type, auto-serialize, hidden, collection(), pagination, with_()#96
Merged
Conversation
Redesign the JSON:API module with a new generic `JsonResource[T]` base class that replaces `JsonAPIResponse` / `JsonAPIListResponse` with zero-boilerplate auto-serialization, hidden fields, paginator support, and `with_()` envelope hook. Changes: - Add `JsonResource[T]` with auto-type via `inflection.tableize()`, `__init__(model)`, `to_attributes()` auto-serialize from `model.serialize()` minus id+hidden, `hidden` class var blacklist, `with_()` top-level envelope merge, and `JsonResource.collection(items)` factory supporting both plain lists and LengthAwarePaginator / SimplePaginator (pagination meta auto-populated). - Add `_ResourceCollection` replacing `JsonAPIListResponse` internals with paginator-aware `to_meta()`. - Retain all existing functionality; add aliases `JsonAPIResponse = JsonResource` and `JsonAPIListResponse = _ResourceCollection` for backward compatibility. - Export `JsonResource` and `_ResourceCollection` from `fastapi_startkit.jsonapi`. - Add 47 new tests covering auto-type, init, auto-serialize, hidden, with_(), collection(list), collection(LengthAwarePaginator), collection(SimplePaginator), backward-compat aliases, and full document shape. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…public interface Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e stripped model.serialize() output is passed through as-is; only fields explicitly listed in hidden=[] are excluded from data.attributes. Previously 'id' was always stripped, but there is no reason to silently drop it — callers who want to hide it can add it to hidden=['id']. Update tests and docstrings accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rialize() args
serialize() no longer accepts include/fields arguments. Use the fluent chain
API instead:
PostResource(post).include("author").fields("posts", ["title"]).serialize()
PostResource.collection(posts).include("author").fields("posts", ["title"])
Chain methods return self and store state in _chain_include / _chain_fields.
When a resource is returned directly from a FastAPI endpoint without calling
chain methods, ?include= and ?fields[*]= query params are still parsed
automatically from the live request (existing auto-parse behavior is preserved).
- Add .include(*relationships) and .fields(type_name, field_list) to both
JsonResource and _ResourceCollection
- Add _chain_state_set flag; __call__ skips query-string parsing when set
- Remove include/fields params from serialize() on both classes
- Update all tests to use chain API
- Drop from_request() — redundant since __call__ handles query strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old: .fields("posts", ["title", "created_at"]).fields("users", ["name"])
New: .fields("title", "created_at", "users.name")
- Plain names apply to the primary resource type (self.type / _primary_type)
- Dotted names ("type.field") apply to a named related type
- Multiple calls accumulate (setdefault behaviour)
- _ResourceCollection stores _primary_type (set by collection() factory)
- Add 7 dedicated TestFieldsDotNotation tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
30 new tests across 5 test classes that verify JsonResource end-to-end
with actual Model instances (User, Articles) and the SQLite test database:
- TestJsonResourceWithOrmModel: auto-serialize from model.serialize(),
hidden fields excluded, .fields() chain restricts real attributes,
document envelope shape
- TestJsonResourceCollectionWithOrm: collection() wraps User.all(),
correct type/id per item, .fields() chain on collection
- TestJsonResourceWithLengthAwarePaginator: User.paginate() meta
(total, per_page, current_page, last_page, next_page, no-next on last)
- TestJsonResourceWithSimplePaginator: User.simple_paginate() meta
(next_page present, no total/last_page)
- TestJsonResourceRelationshipsWithOrm: to_relationships() with real
models, include("author") sideloads, .fields("users.name") restricts
included resource
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three equivalent forms are now supported:
def to_relationships(self):
return {
"author": UserResource, # class → auto-wrap model.author
"comments": lambda: CommentResource.collection(...), # callable → call it
"tag": TagResource(self.model.tag), # instance → use directly
}
Also: ResourceCollection as a relationship value produces JSON:API array
linkage {"data": [...]} and sideloads all items when included.
Implementation:
- _resolve_rel(key, value) uses inspect.isfunction/ismethod to distinguish
lambdas from JsonResource instances (which are ASGI-callable)
- _resolved_relationships() normalises to_relationships() before use
- _build_data() and _collect_included() use the normalised dict
- Remove relationships class-level dict (was old API)
Tests: 7 new TestRelationshipForms cases in test_response.py;
ORM test updated to use class-reference form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rceCollection() needed - Class reference + list/Collection attribute → ResourceClass.collection(items) called automatically - Lambda returning a list → auto-wrapped in ResourceCollection - Plain list of JsonResource instances → auto-wrapped in ResourceCollection - ResourceCollection / single JsonResource instance → used as-is Fix _is_many() to use obj.__dict__ instead of hasattr(): ORM models override __getattr__ to return None for any attribute, making hasattr(model, "_items") always True. Checking __dict__ directly bypasses __getattr__ and only returns True when _items is actually set on the instance (i.e. a real ORM Collection, not a Model). Tests: 3 new TestRelationshipForms cases (plain list, lambda with .collection(), class-ref with list attribute). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…=collection Remove auto-detection of list/Collection attributes for class references. The rules are now explicit and unambiguous: "author": UserResource, # single resource "comments": lambda: CommentResource.collection(..) # has-many / collection - Remove _is_many() helper (no longer needed) - _resolve_rel: class reference always calls ResourceClass(model.attr); lambda is called as-is (user calls .collection() inside) - Drop plain-list and class-ref-auto-collect test cases - Add lambda-with-collection test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
JsonResource[T]— new generic base class replacingJsonAPIResponse/JsonAPIListResponse__init__(self, model)storesself.modeland auto-setsself.id = model.idinflection.tableize()(AgentResource→"agents")to_attributes()callsmodel.serialize()and strips"id"+hiddenfieldshidden = [...]class variable blacklists sensitive fields from auto-serializewith_()method merges extra top-level envelope keys into the JSON:API document (Pythonicwithavoidance)JsonResource.collection(items)class method — accepts a plain list orLengthAwarePaginator/SimplePaginator; pagination meta (total,per_page,current_page,last_page,next_page,previous_page) is auto-populated_ResourceCollection— new collection wrapper with paginator-awareto_meta()JsonAPIResponse = JsonResource,JsonAPIListResponse = _ResourceCollection— all existing code continues to work unchangedtests/jsonapi/test_json_resource.pycovering all new featuresfastapi_startkit.github.io.git/docs/jsonapi.mdwith sidebar entry added to.vitepress/config.mts(docs are in a separate gitignored repo, changes applied directly)Test plan
uv run pytest tests/jsonapi/ -v→ 122 passed (75 existing + 47 new)ruff check+ruff formatpass with no issuesJsonAPIResponse/JsonAPIListResponsetests pass unchanged🤖 Generated with Claude Code