From f6a6d7a65be8c4f2a35aeb7778f1b1e669c1199e Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 5 May 2026 11:37:04 +0200 Subject: [PATCH] docs: add typed models concept guide Document the v3 typed-model surface: how response Pydantic models work, how to pass structured input as dicts (with matching TypedDicts), forward compatibility under extra='allow', and where to browse all generated models in the API reference. --- docs/02_concepts/12_typed_models.mdx | 73 +++++++++++++++++++ .../code/12_typed_models_access_async.py | 18 +++++ .../code/12_typed_models_access_sync.py | 18 +++++ .../code/12_typed_models_input_async.py | 17 +++++ .../code/12_typed_models_input_sync.py | 17 +++++ 5 files changed, 143 insertions(+) create mode 100644 docs/02_concepts/12_typed_models.mdx create mode 100644 docs/02_concepts/code/12_typed_models_access_async.py create mode 100644 docs/02_concepts/code/12_typed_models_access_sync.py create mode 100644 docs/02_concepts/code/12_typed_models_input_async.py create mode 100644 docs/02_concepts/code/12_typed_models_input_sync.py diff --git a/docs/02_concepts/12_typed_models.mdx b/docs/02_concepts/12_typed_models.mdx new file mode 100644 index 00000000..1fcf1c21 --- /dev/null +++ b/docs/02_concepts/12_typed_models.mdx @@ -0,0 +1,73 @@ +--- +id: typed-models +title: Typed models +description: Resource client methods return Pydantic models generated from the Apify OpenAPI spec, with IDE autocomplete, runtime validation, and forward-compatible field access. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import CodeBlock from '@theme/CodeBlock'; +import ApiLink from '@site/src/components/ApiLink'; + +import AccessAsyncExample from '!!raw-loader!./code/12_typed_models_access_async.py'; +import AccessSyncExample from '!!raw-loader!./code/12_typed_models_access_sync.py'; + +import InputAsyncExample from '!!raw-loader!./code/12_typed_models_input_async.py'; +import InputSyncExample from '!!raw-loader!./code/12_typed_models_input_sync.py'; + +Resource client methods return [Pydantic](https://docs.pydantic.dev/) models generated directly from the [Apify OpenAPI specification](https://docs.apify.com/api/openapi.json). You get IDE autocompletion, runtime validation of API responses, and a Python-idiomatic snake_case interface on top of the underlying camelCase API — without having to handwrite or maintain any of the model code. + +## Accessing response fields + +Every method that returns a structured payload returns a Pydantic model. Fields are accessed using their Python snake_case names regardless of the camelCase used by the API, and the static type of each field comes through to your editor. + + + + + {AccessAsyncExample} + + + + + {AccessSyncExample} + + + + +Date strings are automatically parsed into timezone-aware `datetime.datetime` objects, enums into `Literal` aliases, and nested objects into their own typed models, so you can compose attribute access without manual conversion. + +## Providing structured input + +A Pydantic model returned from one client call can be passed directly into any other method that accepts the same shape — useful for round-trip flows where you read a resource, tweak it, and write it back. + +For input you construct yourself, plain dictionaries work on every input method. Each input shape has a matching [`TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) that documents the expected keys. + + + + + {InputAsyncExample} + + + + + {InputSyncExample} + + + + +## Forward compatibility + +Generated models are configured with `extra='allow'`. Any new fields the API starts returning in the future are preserved on the model instance — they simply do not yet have a typed attribute. Upgrading the client to pick up a newer OpenAPI spec is a non-breaking change for code that reads existing fields. + +## Browsing all models + +The full list of generated models and TypedDicts is available in the [API reference](/api/client/python/reference) under the **Models** and **Typed dicts** groups. + +## Methods that return plain types + +A few endpoints intentionally return plain Python types instead of Pydantic models because their payloads are user-defined or inherently unstructured: + +- `DatasetClient.list_items()` returns a `DatasetItemsPage` whose `items` field is `list[dict[str, Any]]`. Dataset items follow the [Actor output schema](https://docs.apify.com/platform/actors/development/actor-definition/output-schema), which the client cannot know in advance. +- `KeyValueStoreClient.get_record()` returns a `dict` with `key`, `value`, and `content_type` keys. The shape of `value` is determined by the record's content type. + +For background on the migration from plain dicts to typed models, see [Upgrading to v3](../04_upgrading/upgrading_to_v3.mdx). diff --git a/docs/02_concepts/code/12_typed_models_access_async.py b/docs/02_concepts/code/12_typed_models_access_async.py new file mode 100644 index 00000000..6d0caeb1 --- /dev/null +++ b/docs/02_concepts/code/12_typed_models_access_async.py @@ -0,0 +1,18 @@ +from apify_client import ApifyClientAsync + +TOKEN = 'MY-APIFY-TOKEN' + + +async def main() -> None: + apify_client = ApifyClientAsync(TOKEN) + + # `get` returns an `Actor` Pydantic model — fields are typed and IDE-completable. + actor = await apify_client.actor('apify/hello-world').get() + if actor is None: + return + + print(actor.id) # str + print(actor.username) # str + print(actor.is_public) # bool + print(actor.created_at) # datetime.datetime (timezone-aware) + print(actor.stats.total_runs) # int — nested model, attribute access all the way down diff --git a/docs/02_concepts/code/12_typed_models_access_sync.py b/docs/02_concepts/code/12_typed_models_access_sync.py new file mode 100644 index 00000000..e616d1ec --- /dev/null +++ b/docs/02_concepts/code/12_typed_models_access_sync.py @@ -0,0 +1,18 @@ +from apify_client import ApifyClient + +TOKEN = 'MY-APIFY-TOKEN' + + +def main() -> None: + apify_client = ApifyClient(TOKEN) + + # `get` returns an `Actor` Pydantic model — fields are typed and IDE-completable. + actor = apify_client.actor('apify/hello-world').get() + if actor is None: + return + + print(actor.id) # str + print(actor.username) # str + print(actor.is_public) # bool + print(actor.created_at) # datetime.datetime (timezone-aware) + print(actor.stats.total_runs) # int — nested model, attribute access all the way down diff --git a/docs/02_concepts/code/12_typed_models_input_async.py b/docs/02_concepts/code/12_typed_models_input_async.py new file mode 100644 index 00000000..91568a71 --- /dev/null +++ b/docs/02_concepts/code/12_typed_models_input_async.py @@ -0,0 +1,17 @@ +from apify_client import ApifyClientAsync + +TOKEN = 'MY-APIFY-TOKEN' + + +async def main() -> None: + apify_client = ApifyClientAsync(TOKEN) + rq_client = apify_client.request_queue('REQUEST-QUEUE-ID') + + # Plain dict — keys may be snake_case or camelCase. + await rq_client.add_request( + { + 'url': 'https://example.com', + 'unique_key': 'https://example.com', + 'method': 'GET', + } + ) diff --git a/docs/02_concepts/code/12_typed_models_input_sync.py b/docs/02_concepts/code/12_typed_models_input_sync.py new file mode 100644 index 00000000..71c08022 --- /dev/null +++ b/docs/02_concepts/code/12_typed_models_input_sync.py @@ -0,0 +1,17 @@ +from apify_client import ApifyClient + +TOKEN = 'MY-APIFY-TOKEN' + + +def main() -> None: + apify_client = ApifyClient(TOKEN) + rq_client = apify_client.request_queue('REQUEST-QUEUE-ID') + + # Plain dict — keys may be snake_case or camelCase. + rq_client.add_request( + { + 'url': 'https://example.com', + 'unique_key': 'https://example.com', + 'method': 'GET', + } + )