Advanced config types
In some cases, you may want to define a more complex config schema for your assets and ops. For example, you may want to define a config schema that takes in a list of files or complex data. In this guide, we'll walk through some common patterns for defining more complex config schemas.
Attaching metadata to config fields
Config fields can be annotated with metadata, which can be used to provide additional information about the field, using the Pydantic Field class.
For example, we can annotate a config field with a description, which will be displayed in the documentation for the config field. We can add a value range to a field, which will be validated when config is specified.
    from dagster import Config
    from pydantic import Field
    class MyMetadataConfig(Config):
        person_name: str = Field(description="The name of the person to greet")
        age: int = Field(gt=0, lt=100, description="The age of the person to greet")
    # errors, since age is not in the valid range!
    MyMetadataConfig(person_name="Alice", age=200)
Defaults and optional config fields
Config fields can have an attached default value. Fields with defaults are not required, meaning they do not need to be specified when constructing the config object.
For example, we can attach a default value of "hello" to the greeting_phrase field, and can construct MyAssetConfig without specifying a phrase. Fields which are marked as Optional, such as person_name, implicitly have a default value of None, but can also be explicitly set to None as in the example below.
    from typing import Optional
    from dagster import asset, Config, materialize, RunConfig
    from pydantic import Field
    class MyAssetConfig(Config):
        person_name: Optional[str] = None
        # can pass default to pydantic.Field to attach metadata to the field
        greeting_phrase: str = Field(
            default="hello", description="The greeting phrase to use."
        )
    @asset
    def greeting(config: MyAssetConfig) -> str:
        if config.person_name:
            return f"{config.greeting_phrase} {config.person_name}"
        else:
            return config.greeting_phrase
    asset_result = materialize(
        [greeting],
        run_config=RunConfig({"greeting": MyAssetConfig()}),
    )
Required config fields
By default, fields which are typed as Optional are not required to be specified in the config, and have an implicit default value of None. If you want to require that a field be specified in the config, you may use an ellipsis (...) to require that a value be passed.
    from typing import Optional, Callable
    from dagster import asset, Config
    from pydantic import Field
    class MyAssetConfig(Config):
        # ellipsis indicates that even though the type is Optional,
        # an input is required
        person_first_name: Optional[str] = ...
        # ellipsis can also be used with pydantic.Field to attach metadata
        person_last_name: Optional[Callable] = Field(
            default=..., description="The last name of the person to greet"
        )
    @asset
    def goodbye(config: MyAssetConfig) -> str:
        full_name = f"{config.person_first_name} {config.person_last_name}".strip()
        if full_name:
            return f"Goodbye, {full_name}"
        else:
            return "Goodbye"
    # errors, since person_first_name and person_last_name are required
    goodbye(MyAssetConfig())
    # works, since both person_first_name and person_last_name are provided
    goodbye(MyAssetConfig(person_first_name="Alice", person_last_name=None))
Basic data structures
Basic Python data structures can be used in your config schemas along with nested versions of these data structures. The data structures which can be used are:
- List
- Dict
- Mapping
For example, we can define a config schema that takes in a list of user names and a mapping of user names to user scores.
    from dagster import Config, materialize, asset, RunConfig
    class MyDataStructuresConfig(Config):
        user_names: list[str]
        user_scores: dict[str, int]
    @asset
    def scoreboard(config: MyDataStructuresConfig): ...
    result = materialize(
        [scoreboard],
        run_config=RunConfig(
            {
                "scoreboard": MyDataStructuresConfig(
                    user_names=["Alice", "Bob"],
                    user_scores={"Alice": 10, "Bob": 20},
                )
            }
        ),
    )
Nested schemas
Schemas can be nested in one another, or in basic Python data structures.
Here, we define a schema which contains a mapping of user names to complex user data objects.
    from dagster import asset, materialize, Config, RunConfig
    class UserData(Config):
        age: int
        email: str
        profile_picture_url: str
    class MyNestedConfig(Config):
        user_data: dict[str, UserData]
    @asset
    def average_age(config: MyNestedConfig): ...
    result = materialize(
        [average_age],
        run_config=RunConfig(
            {
                "average_age": MyNestedConfig(
                    user_data={
                        "Alice": UserData(
                            age=10,
                            email="alice@gmail.com",
                            profile_picture_url=...,
                        ),
                        "Bob": UserData(
                            age=20,
                            email="bob@gmail.com",
                            profile_picture_url=...,
                        ),
                    }
                )
            }
        ),
    )
Permissive schemas
By default, Config schemas are strict, meaning that they will only accept fields that are explicitly defined in the schema. This can be cumbersome if you want to allow users to specify arbitrary fields in their config. For this purpose, you can use the PermissiveConfig base class, which allows arbitrary fields to be specified in the config.
    from dagster import asset, PermissiveConfig
    from typing import Optional
    import requests
    class FilterConfig(PermissiveConfig):
        title: Optional[str] = None
        description: Optional[str] = None
    @asset
    def filtered_listings(config: FilterConfig):
        # extract all config fields, including those not defined in the schema
        url_params = config.dict()
        return requests.get("https://my-api.com/listings", params=url_params).json()
    # can pass in any fields, including those not defined in the schema
    filtered_listings(FilterConfig(title="hotel", beds=4))  # type: ignore
Union types
Union types are supported using Pydantic discriminated unions. Each union type must be a subclass of Config. The discriminator argument to Field specifies the field that will be used to determine which union type to use. Discriminated unions provide comparable functionality to the Selector type in the legacy Dagster config APIs.
Here, we define a config schema which takes in a pet field, which can be either a Cat or a Dog, as indicated by the pet_type field.
    from dagster import asset, materialize, Config, RunConfig
    from pydantic import Field
    from typing import Union
    from typing_extensions import Literal
    class Cat(Config):
        pet_type: Literal["cat"] = "cat"
        meows: int
    class Dog(Config):
        pet_type: Literal["dog"] = "dog"
        barks: float
    class ConfigWithUnion(Config):
        pet: Union[Cat, Dog] = Field(discriminator="pet_type")
    @asset
    def pet_stats(config: ConfigWithUnion):
        if isinstance(config.pet, Cat):
            return f"Cat meows {config.pet.meows} times"
        else:
            return f"Dog barks {config.pet.barks} times"
    result = materialize(
        [pet_stats],
        run_config=RunConfig(
            {
                "pet_stats": ConfigWithUnion(
                    pet=Cat(meows=10),
                )
            }
        ),
    )
YAML and config dictionary representations of union types
The YAML or config dictionary representation of a discriminated union is structured slightly differently than the Python representation. In the YAML representation, the discriminator key is used as the key for the union type's dictionary. For example, a Cat object would be represented as:
pet:
  cat:
    meows: 10
In the config dictionary representation, the same pattern is used:
{
    "pet": {
        "cat": {
            "meows": 10,
        }
    }
}
Enum types
Python enums which subclass Enum are supported as config fields. Here, we define a schema that takes in a list of users, whose roles are specified as enum values:
    from dagster import Config, RunConfig, op, job
    from enum import Enum
    class UserPermissions(Enum):
        GUEST = "guest"
        MEMBER = "member"
        ADMIN = "admin"
    class ProcessUsersConfig(Config):
        users_list: dict[str, UserPermissions]
    @op
    def process_users(config: ProcessUsersConfig):
        for user, permission in config.users_list.items():
            if permission == UserPermissions.ADMIN:
                print(f"{user} is an admin")  
    @job
    def process_users_job():
        process_users()
    op_result = process_users_job.execute_in_process(
        run_config=RunConfig(
            {
                "process_users": ProcessUsersConfig(
                    users_list={
                        "Bob": UserPermissions.GUEST,
                        "Alice": UserPermissions.ADMIN,
                    }
                )
            }
        ),
    )
YAML and config dictionary representations of enum types
The YAML or config dictionary representation of a Python enum uses the enum's name. For example, a YAML specification of the user list above would be:
users_list:
  Bob: GUEST
  Alice: ADMIN
In the config dictionary representation, the same pattern is used:
{
    "users_list": {
        "Bob": "GUEST",
        "Alice": "ADMIN",
    }
}
Validated config fields
Config fields can have custom validation logic applied using Pydantic validators. Pydantic validators are defined as methods on the config class, and are decorated with the @validator decorator. These validators are triggered when the config class is instantiated. In the case of config defined at runtime, a failing validator will not prevent the launch button from being pressed, but will raise an exception and prevent run start.
Here, we define some validators on a configured user's name and username, which will throw exceptions if incorrect values are passed in the launchpad or from a schedule or sensor.
    from dagster import Config, RunConfig, op, job
    from pydantic import validator
    class UserConfig(Config):
        name: str
        username: str
        @validator("name")
        def name_must_contain_space(cls, v):
            if " " not in v:
                raise ValueError("must contain a space")
            return v.title()
        @validator("username")
        def username_alphanumeric(cls, v):
            assert v.isalnum(), "must be alphanumeric"
            return v
    executed = {}
    @op
    def greet_user(config: UserConfig) -> None:
        print(f"Hello {config.name}!")  
        executed["greet_user"] = True
    @job
    def greet_user_job() -> None:
        greet_user()
    # Input is valid, so this will work
    op_result = greet_user_job.execute_in_process(
        run_config=RunConfig(
            {"greet_user": UserConfig(name="Alice Smith", username="alice123")}
        ),
    )
    # Name has no space, so this will fail
    op_result = greet_user_job.execute_in_process(
        run_config=RunConfig(
            {"greet_user": UserConfig(name="John", username="johndoe44")}
        ),
    )