Skip to main content
Version: Next

Typed models

Resource client methods return Pydantic models generated directly from the Apify OpenAPI specification. You get IDE autocompletion, runtime validation of API responses, and a Python-idiomatic snake_case interface on top of the underlying camelCase API.

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.

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

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.

URL fields

Fields holding a URL, such as Run.container_url or Dataset.console_url, are typed as AnyUrl. Their values are validated and normalized, so the string you read back can differ from the one the API sent. Both forms denote the same URL under RFC 3986, they're just different strings.

from pydantic import AnyUrl

# An empty path becomes '/'.
str(AnyUrl('https://abc123.runs.apify.net')) # 'https://abc123.runs.apify.net/'

# The host is lowercased.
str(AnyUrl('https://EXAMPLE.com/Path')) # 'https://example.com/Path'

# A default port is dropped.
str(AnyUrl('https://example.com:443/path')) # 'https://example.com/path'

# An internationalized host is punycoded.
str(AnyUrl('https://www.žluty.cz')) # 'https://www.xn--luty-kbb.cz/'

# Unsafe characters are percent-encoded.
str(AnyUrl('https://example.com/a b')) # 'https://example.com/a%20b'

Because of the normalization, compare URLs in their normalized form instead of as raw strings. AnyUrl compares the parsed URL, not the input text.

run = client.run('my-run-id').get()
stored_url = 'https://abc123.runs.apify.net'

# Wrong, compares a raw string with a normalized one.
stored_url == str(run.container_url) # False

# Right, both sides are normalized before the comparison.
AnyUrl(stored_url) == run.container_url # True

To build a longer URL out of a URL field, use urljoin rather than string concatenation. It gives the same result whether or not the base ends with a slash.

from urllib.parse import urljoin

urljoin(str(run.container_url), 'status') # 'https://abc123.runs.apify.net/status'

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 that documents the expected keys.

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',
}
)

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 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:

For background on the migration from plain dicts to typed models, see Upgrading to v3.