Skip to content

feat(jsonapi): JsonResource[T] generic base with auto-type, auto-serialize, hidden, collection(), pagination, with_()#96

Merged
bedus-creation merged 10 commits into
mainfrom
task/feat-jsonapi-json-resource
Jun 8, 2026
Merged

feat(jsonapi): JsonResource[T] generic base with auto-type, auto-serialize, hidden, collection(), pagination, with_()#96
bedus-creation merged 10 commits into
mainfrom
task/feat-jsonapi-json-resource

Conversation

@bedus-creation

Copy link
Copy Markdown
Contributor

Summary

  • JsonResource[T] — new generic base class replacing JsonAPIResponse / JsonAPIListResponse
    • __init__(self, model) stores self.model and auto-sets self.id = model.id
    • Auto-type derived from class name via inflection.tableize() (AgentResource"agents")
    • Default to_attributes() calls model.serialize() and strips "id" + hidden fields
    • hidden = [...] class variable blacklists sensitive fields from auto-serialize
    • with_() method merges extra top-level envelope keys into the JSON:API document (Pythonic with avoidance)
    • JsonResource.collection(items) class method — accepts a plain list or LengthAwarePaginator / SimplePaginator; pagination meta (total, per_page, current_page, last_page, next_page, previous_page) is auto-populated
  • _ResourceCollection — new collection wrapper with paginator-aware to_meta()
  • Backward-compatible aliases: JsonAPIResponse = JsonResource, JsonAPIListResponse = _ResourceCollection — all existing code continues to work unchanged
  • 47 new tests in tests/jsonapi/test_json_resource.py covering all new features
  • Docs created at fastapi_startkit.github.io.git/docs/jsonapi.md with 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 format pass with no issues
  • All backward-compat JsonAPIResponse / JsonAPIListResponse tests pass unchanged

🤖 Generated with Claude Code

bedus-creation and others added 2 commits June 7, 2026 22:48
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>
bedus-creation and others added 8 commits June 7, 2026 23:04
…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>
@bedus-creation bedus-creation merged commit f236b81 into main Jun 8, 2026
3 checks passed
@bedus-creation bedus-creation deleted the task/feat-jsonapi-json-resource branch June 8, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant