|
| 1 | +--- |
| 2 | +id: input-validation |
| 3 | +title: Input validation with Pydantic |
| 4 | +description: Parse, validate, and type your Actor's input with Pydantic models instead of reaching into a raw dictionary. |
| 5 | +--- |
| 6 | + |
| 7 | +import CodeBlock from '@theme/CodeBlock'; |
| 8 | +import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; |
| 9 | +import ApiLink from '@theme/ApiLink'; |
| 10 | + |
| 11 | +import RawInputExample from '!!raw-loader!roa-loader!./code/11_raw_input.py'; |
| 12 | +import PydanticExample from '!!raw-loader!roa-loader!./code/11_pydantic.py'; |
| 13 | +import HttpUrlExample from '!!raw-loader!./code/11_http_url.py'; |
| 14 | +import ModelValidatorExample from '!!raw-loader!./code/11_model_validator.py'; |
| 15 | +import SecretStrExample from '!!raw-loader!./code/11_secret_str.py'; |
| 16 | + |
| 17 | +In this guide, you'll learn how to validate your Apify Actor's input with [Pydantic](https://docs.pydantic.dev/), so that your code works with a typed, guaranteed-valid object instead of a raw dictionary. |
| 18 | + |
| 19 | +## Introduction |
| 20 | + |
| 21 | +An Actor reads its input with <ApiLink to="class/Actor#get_input">`Actor.get_input`</ApiLink>, which returns the input record as a plain `dict`. Working with that dictionary directly is fragile: |
| 22 | + |
| 23 | +<RunnableCodeBlock className="language-python" language="python"> |
| 24 | + {RawInputExample} |
| 25 | +</RunnableCodeBlock> |
| 26 | + |
| 27 | +- There are no type guarantees. `max_results` can arrive as the string `"10"` or `None` and you won't know until something breaks. |
| 28 | +- There's no validation. Nothing stops `max_results` from being `0` or `-5`, or `search_terms` from being empty. |
| 29 | +- A typo in a key, like `maxResult` instead of `maxResults`, silently falls back to the default instead of failing. |
| 30 | +- Defaults are scattered across the codebase, and your editor can't autocomplete the fields or catch mistakes. |
| 31 | + |
| 32 | +[Pydantic](https://docs.pydantic.dev/) solves all of these problems. You declare the shape of your input once as a model, and Pydantic parses the raw dictionary into a typed object, applies defaults, enforces constraints, and produces clear error messages when the input doesn't match. |
| 33 | + |
| 34 | +To use Pydantic, install it into your Actor's environment: |
| 35 | + |
| 36 | +```bash |
| 37 | +pip install pydantic |
| 38 | +``` |
| 39 | + |
| 40 | +## Example Actor |
| 41 | + |
| 42 | +The following Actor declares its input as a Pydantic `BaseModel`, validates the raw input against it, and then works with a fully typed object. On invalid input it fails fast with a readable error. On valid input it logs the normalized values and stores them as the Actor's output. |
| 43 | + |
| 44 | +<RunnableCodeBlock className="language-python" language="python"> |
| 45 | + {PydanticExample} |
| 46 | +</RunnableCodeBlock> |
| 47 | + |
| 48 | +### About the model |
| 49 | + |
| 50 | +- Apify input fields conventionally use camel case (`maxResults`), while Python attributes use snake case (`max_results`). Since every field follows that convention, `alias_generator=to_camel` derives the camel case alias for the whole model at once, instead of spelling out `Field(alias=...)` on each field. `populate_by_name=True` lets the model accept either spelling, which is handy in tests. |
| 51 | +- A field without a default (`search_terms`) is required. A field with a default (`max_results`) is optional. There's a single, obvious place where every default lives. |
| 52 | +- `ge=1, le=100` enforces a numeric range, `min_length=1` rejects an empty list, and `Literal['json', 'csv']` restricts a field to a fixed set of choices, mirroring an `enum` in the input schema. |
| 53 | +- The `field_validator` normalizes the search terms (trimming whitespace, dropping empties) and rejects input that has nothing left. The rest of your code never has to repeat those checks. |
| 54 | +- `extra='ignore'` means adding a new field to your input schema won't break an older Actor build that doesn't know about it yet. Use `extra='forbid'` instead if you prefer to reject anything unexpected. |
| 55 | + |
| 56 | +### About the validation |
| 57 | + |
| 58 | +- `model_validate` parses the raw dictionary into a typed `ActorInput` instance. It fills in defaults and guarantees every field is valid, or raises a `ValidationError` that describes every problem at once. |
| 59 | +- Catching that error, logging a readable summary, and re-raising makes the Actor fail fast with a clear explanation right at the start, rather than crashing with an obscure error somewhere deep in the run. Because the body runs inside `async with Actor:`, the re-raised exception automatically marks the run as `FAILED`. |
| 60 | +- The error messages refer to the fields by their input-schema aliases. For invalid input like `{"searchTerms": [], "maxResults": 999, "outputFormat": "xml"}`, the log shows exactly what's wrong: |
| 61 | + |
| 62 | + ```text |
| 63 | + The Actor input is invalid: |
| 64 | + 3 validation errors for ActorInput |
| 65 | + searchTerms |
| 66 | + List should have at least 1 item after validation, not 0 ... |
| 67 | + maxResults |
| 68 | + Input should be less than or equal to 100 ... |
| 69 | + outputFormat |
| 70 | + Input should be 'json' or 'csv' ... |
| 71 | + ``` |
| 72 | + |
| 73 | +Once validation passes, the rest of `main` works with `actor_input.search_terms`, `actor_input.max_results`, and `actor_input.output_format`, all correctly typed, with editor autocompletion and static type checking. |
| 74 | + |
| 75 | +## Relationship to the input schema |
| 76 | + |
| 77 | +Pydantic validation complements the Actor's [input schema](https://docs.apify.com/platform/actors/development/input-schema) (`.actor/input_schema.json`). It doesn't replace it. The two serve different layers: |
| 78 | + |
| 79 | +- The input schema drives the [Apify Console](https://console.apify.com/) form, documents the fields for your users, and lets the platform validate input before the run even starts. Keep declaring your fields there. |
| 80 | +- The Pydantic model validates the input again inside your Python code, where it gives you a typed object, IDE support, and richer rules (normalization, cross-field checks, custom formats) that the input schema can't express. It's also your safety net for runs started programmatically by [another Actor](../concepts/interacting-with-other-actors) or executed [locally](https://docs.apify.com/cli/docs/reference#apify-run), and for keeping the two definitions honest with each other. |
| 81 | + |
| 82 | +Keep the model's aliases in sync with the field keys in `input_schema.json`, and the two definitions describe the same input from both sides. |
| 83 | + |
| 84 | +## Useful validation features |
| 85 | + |
| 86 | +Pydantic offers extra features for validating Actor input. For the full set of types, constraints, and validators, see the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/models/). |
| 87 | + |
| 88 | +### Format-validated types |
| 89 | + |
| 90 | +For common string formats, for example `HttpUrl` for URLs or `EmailStr` for e-mail addresses, use format-validated types: |
| 91 | + |
| 92 | +<CodeBlock className="language-python"> |
| 93 | + {HttpUrlExample} |
| 94 | +</CodeBlock> |
| 95 | + |
| 96 | +### Cross-field validation |
| 97 | + |
| 98 | +When one field's validity depends on another, use `model_validator`: |
| 99 | + |
| 100 | +<CodeBlock className="language-python"> |
| 101 | + {ModelValidatorExample} |
| 102 | +</CodeBlock> |
| 103 | + |
| 104 | +### Secret input fields |
| 105 | + |
| 106 | +The platform decrypts [secret input fields](https://docs.apify.com/platform/actors/development/secret-input) for you before <ApiLink to="class/Actor#get_input">`Actor.get_input`</ApiLink> returns, so you receive plaintext. To keep them from leaking into logs or `model_dump()` output, wrap such fields in Pydantic's `SecretStr` and read the plaintext with `get_secret_value()` when you actually need it: |
| 107 | + |
| 108 | +<CodeBlock className="language-python"> |
| 109 | + {SecretStrExample} |
| 110 | +</CodeBlock> |
| 111 | + |
| 112 | +## Conclusion |
| 113 | + |
| 114 | +In this guide, you learned how to validate Actor input with Pydantic: declaring the input as a model with aliases, defaults, and constraints, parsing the raw input with `model_validate`, failing fast with a readable error when the input is invalid, and working with a typed object for the rest of the run. To get started with your own Actors, see the [Actor templates](https://apify.com/templates/categories/python). If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy validating! |
| 115 | + |
| 116 | +## Additional resources |
| 117 | + |
| 118 | +- [Pydantic: Official documentation](https://docs.pydantic.dev/) |
| 119 | +- [Pydantic: Models](https://docs.pydantic.dev/latest/concepts/models/) |
| 120 | +- [Pydantic: Validators](https://docs.pydantic.dev/latest/concepts/validators/) |
| 121 | +- [Apify: Actor input](https://docs.apify.com/platform/actors/running/input) |
| 122 | +- [Apify: Input schema specification](https://docs.apify.com/platform/actors/development/input-schema) |
0 commit comments