Models Reference¶
Complete reference for strapi-kit's type-safe models and query builder.
Table of Contents¶
Overview¶
strapi-kit provides a complete type-safe interface for building Strapi queries and parsing responses. The models work with both Strapi v4 and v5, automatically detecting the version and normalizing responses to a consistent format.
Key Components¶
StrapiQuery: Main query builder combining filters, sort, pagination, etc.FilterBuilder: Fluent API for building complex filtersPopulate: Configure relation expansion with nested filteringNormalizedEntity: Version-agnostic response model
Query Builder¶
StrapiQuery¶
The main interface for building complete queries:
Methods¶
.filter(filters: FilterBuilder) -> StrapiQuery¶
Add filter conditions:
.sort_by(field: str, direction: SortDirection) -> StrapiQuery¶
Add primary sort field:
from strapi_kit.models import SortDirection
query = StrapiQuery().sort_by("publishedAt", SortDirection.DESC)
.then_sort_by(field: str, direction: SortDirection) -> StrapiQuery¶
Add secondary sort fields:
.paginate(...) -> StrapiQuery¶
Add pagination (page-based or offset-based):
# Page-based
query = StrapiQuery().paginate(page=1, page_size=25)
# Offset-based
query = StrapiQuery().paginate(start=0, limit=50)
# Disable count
query = StrapiQuery().paginate(page=1, page_size=100, with_count=False)
.populate(populate: Populate) -> StrapiQuery¶
Advanced population configuration:
from strapi_kit.models import Populate
query = StrapiQuery().populate(
Populate()
.add_field("author", fields=["name", "email"])
.add_field("category")
)
.populate_all() -> StrapiQuery¶
Populate all first-level relations:
.populate_fields(fields: list[str]) -> StrapiQuery¶
Populate specific fields (simple):
.select(fields: list[str]) -> StrapiQuery¶
Select specific fields to return:
.with_locale(locale: str) -> StrapiQuery¶
Set locale for i18n content:
.with_publication_state(state: PublicationState) -> StrapiQuery¶
Filter by publication state:
from strapi_kit.models import PublicationState
query = StrapiQuery().with_publication_state(PublicationState.LIVE)
.to_query_params() -> dict[str, Any]¶
Convert to query parameters for HTTP requests:
Filter Operators¶
FilterBuilder¶
Fluent API for building filters with 24 operators:
Equality Operators¶
.eq(field, value) # Equal (case-sensitive)
.eqi(field, value) # Equal (case-insensitive)
.ne(field, value) # Not equal (case-sensitive)
.nei(field, value) # Not equal (case-insensitive)
Examples:
FilterBuilder().eq("status", "published")
FilterBuilder().eqi("title", "HELLO WORLD")
FilterBuilder().ne("category", "draft")
Comparison Operators¶
.lt(field, value) # Less than
.lte(field, value) # Less than or equal
.gt(field, value) # Greater than
.gte(field, value) # Greater than or equal
Examples:
FilterBuilder().gt("views", 1000)
FilterBuilder().between("price", 10, 100)
FilterBuilder().gte("publishedAt", "2024-01-01")
String Matching Operators¶
.contains(field, value) # Contains substring
.not_contains(field, value) # Does not contain
.containsi(field, value) # Contains (case-insensitive)
.not_containsi(field, value) # Does not contain (case-insensitive)
.starts_with(field, value) # Starts with
.starts_withi(field, value) # Starts with (case-insensitive)
.ends_with(field, value) # Ends with
.ends_withi(field, value) # Ends with (case-insensitive)
Examples:
FilterBuilder().contains("title", "Python")
FilterBuilder().starts_with("slug", "blog-")
FilterBuilder().containsi("description", "tutorial")
Array Operators¶
Examples:
FilterBuilder().in_("status", ["published", "draft"])
FilterBuilder().not_in("category", ["archived", "deleted"])
Null Operators¶
Examples:
FilterBuilder().null("deletedAt") # Match null values
FilterBuilder().null("deletedAt", False) # Match non-null values
FilterBuilder().not_null("publishedAt")
Range Operators¶
Examples:
FilterBuilder().between("price", 10, 100)
FilterBuilder().between("publishedAt", "2024-01-01", "2024-12-31")
Logical Operators¶
Examples:
# OR: category is "tech" OR "science"
FilterBuilder().or_group(
FilterBuilder().eq("category", "tech"),
FilterBuilder().eq("category", "science")
)
# Complex: published AND (views > 1000 OR likes > 500)
FilterBuilder()
.eq("status", "published")
.or_group(
FilterBuilder().gt("views", 1000),
FilterBuilder().gt("likes", 500)
)
# NOT: status is NOT "draft"
FilterBuilder().not_group(
FilterBuilder().eq("status", "draft")
)
Deep Relation Filtering¶
Use dot notation to filter on nested relations:
FilterBuilder().eq("author.name", "John Doe")
FilterBuilder().eq("author.profile.country", "USA")
FilterBuilder().gt("author.posts_count", 10)
Chaining Filters¶
All filter methods return self for chaining:
filters = (FilterBuilder()
.eq("status", "published")
.gt("views", 100)
.contains("title", "Python")
.null("deletedAt"))
Response Models¶
NormalizedEntity¶
Version-agnostic entity representation:
class NormalizedEntity:
id: int # Numeric ID (v4 and v5)
document_id: str | None # Document ID (v5 only, None for v4)
created_at: datetime | None # Creation timestamp
updated_at: datetime | None # Last update timestamp
published_at: datetime | None # Publication timestamp
locale: str | None # Locale code
attributes: dict[str, Any] # All custom fields
Example:
response = client.get_one("articles/1")
article = response.data
print(article.id) # 1
print(article.document_id) # "abc123" (v5) or None (v4)
print(article.attributes["title"]) # "My Article"
print(article.published_at) # datetime object
NormalizedSingleResponse¶
Response for single entity endpoints:
class NormalizedSingleResponse:
data: NormalizedEntity | None # Entity or None if not found
meta: ResponseMeta | None # Response metadata
Example:
response = client.get_one("articles/1")
if response.data:
print(response.data.attributes["title"])
else:
print("Article not found")
NormalizedCollectionResponse¶
Response for collection endpoints:
class NormalizedCollectionResponse:
data: list[NormalizedEntity] # List of entities
meta: ResponseMeta | None # Response metadata
Example:
response = client.get_many("articles")
print(f"Total: {response.meta.pagination.total}")
for article in response.data:
print(article.attributes["title"])
PaginationMeta¶
Pagination metadata:
class PaginationMeta:
page: int | None # Current page number
page_size: int | None # Items per page
page_count: int | None # Total pages
total: int | None # Total items
Example:
response = client.get_many("articles", query)
if response.meta and response.meta.pagination:
p = response.meta.pagination
print(f"Page {p.page} of {p.page_count}")
print(f"Total: {p.total} items")
Normalization¶
V4 vs V5 Structure¶
Strapi v4 (nested attributes):
{
"data": {
"id": 1,
"attributes": {
"title": "Article",
"content": "Body",
"createdAt": "2024-01-01T00:00:00.000Z"
}
}
}
Strapi v5 (flattened):
{
"data": {
"id": 1,
"documentId": "abc123",
"title": "Article",
"content": "Body",
"createdAt": "2024-01-01T00:00:00.000Z"
}
}
Normalized (version-agnostic):
NormalizedEntity(
id=1,
document_id="abc123", # or None for v4
created_at=datetime(2024, 1, 1),
updated_at=None,
published_at=None,
locale=None,
attributes={
"title": "Article",
"content": "Body"
}
)
Conversion Methods¶
# From v4
v4_entity = V4Entity(**v4_response_data)
normalized = NormalizedEntity.from_v4(v4_entity)
# From v5
v5_entity = V5Entity(**v5_response_data)
normalized = NormalizedEntity.from_v5(v5_entity)
The client handles this automatically based on version detection.
Advanced Patterns¶
Nested Population with Filtering¶
Populate relations with their own filters and sorting:
from strapi_kit.models import Populate, FilterBuilder, Sort, SortDirection
query = StrapiQuery().populate(
Populate()
.add_field(
"comments",
filters=FilterBuilder().eq("approved", True),
sort=Sort().by_field("createdAt", SortDirection.DESC),
fields=["content", "author", "createdAt"],
nested=Populate().add_field(
"author",
fields=["name", "avatar"]
)
)
)
Deep Filtering on Multiple Relations¶
Filter on nested relation fields:
query = StrapiQuery().filter(
FilterBuilder()
.eq("author.profile.verified", True)
.eq("category.parent.name", "Technology")
.gt("author.followers_count", 1000)
)
Complex Logical Filters¶
Combine multiple conditions with AND/OR/NOT:
# (status = published) AND ((views > 1000) OR (likes > 500)) AND (NOT archived)
query = StrapiQuery().filter(
FilterBuilder()
.eq("status", "published")
.or_group(
FilterBuilder().gt("views", 1000),
FilterBuilder().gt("likes", 500)
)
.not_group(
FilterBuilder().eq("archived", True)
)
)
Pagination with Sorting¶
Combine pagination and sorting for consistent results:
query = (StrapiQuery()
.filter(FilterBuilder().eq("status", "published"))
.sort_by("publishedAt", SortDirection.DESC)
.then_sort_by("id", SortDirection.ASC) # Stable sort
.paginate(page=1, page_size=25))
Locale-Specific Queries¶
Query content in specific locales:
query = (StrapiQuery()
.filter(FilterBuilder().eq("status", "published"))
.with_locale("fr")
.populate_fields(["localizations"]))
Working with Both APIs¶
Use typed and raw APIs together:
with SyncClient(config) as client:
# Typed API for complex queries
query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
typed_response = client.get_many("articles", query=query)
# Raw API for flexibility
raw_response = client.get("articles", params={"filters[status][$eq]": "published"})
# Both work!
assert len(typed_response.data) == len(raw_response["data"])
Accessing Metadata¶
Extract pagination and other metadata:
response = client.get_many("articles", query)
# Pagination
if response.meta and response.meta.pagination:
total = response.meta.pagination.total
pages = response.meta.pagination.page_count
# Available locales (if i18n enabled)
if response.meta and response.meta.available_locales:
locales = response.meta.available_locales
Type Safety Benefits¶
from strapi_kit.models import NormalizedEntity
response = client.get_one("articles/1")
# IDE autocomplete works!
article: NormalizedEntity = response.data
article.id # int
article.document_id # str | None
article.created_at # datetime | None
article.attributes # dict[str, Any]
# Type checking with mypy
reveal_type(article.id) # Revealed type is 'int'
reveal_type(article.attributes) # Revealed type is 'dict[str, Any]'
Migration Guide¶
From Raw API to Typed API¶
Before (Raw API):
# Manual query building
params = {
"filters[status][$eq]": "published",
"filters[views][$gt]": 100,
"sort": ["publishedAt:desc"],
"pagination[page]": 1,
"pagination[pageSize]": 25,
"populate": ["author", "category"]
}
response = client.get("articles", params=params)
# Manual response parsing
for item in response["data"]:
if "attributes" in item: # v4
title = item["attributes"]["title"]
else: # v5
title = item["title"]
print(title)
After (Typed API):
# Type-safe query building
query = (StrapiQuery()
.filter(FilterBuilder()
.eq("status", "published")
.gt("views", 100))
.sort_by("publishedAt", SortDirection.DESC)
.paginate(page=1, page_size=25)
.populate_fields(["author", "category"]))
response = client.get_many("articles", query=query)
# Normalized response (works with both v4 and v5)
for article in response.data:
print(article.attributes["title"])
Benefits¶
- Type Safety: Full IDE autocomplete and mypy checking
- Version Agnostic: Works with both v4 and v5 automatically
- Cleaner Code: Fluent API is more readable
- Less Error-Prone: Pydantic validates all inputs
- Better Docs: Inline documentation via docstrings