Skip to content

Add a field to openapi schemas

To response schema

Let's say that we decided to expose the creation date of user's account with a created_at field in our API. This is not a breaking change so a new version is completely unnecessary. However, if you believe that you absolutely have to make a new version, then you can simply follow the recommended approach below but add a version change with field didnt exist instruction.

The recommended approach:

  1. Add created_at field into data.head.users.UserResource
  2. Regenerate the versioned schemas

Now you have everything you need at your disposal: field created_at is available in all versions and your users do not even need to do any extra actions. Just make sure that the data for it is available in all versions too. If it's not: make the field optional.

To both request and response schemas

Field is optional

Let's say we want our users to be able to specify a middle name but it is nullable. It is not a breaking change so no new version is necessary whether it is requests or responses.

The recommended approach:

  1. Add a nullable middle_name field into data.head.users.BaseUser
  2. Regenerate the versioned schemas

Field is required

With compatible default value in older versions

Let's say that our users had a field country that defaulted to USA but our product is now used well beyond United States so we want to make this field required in the HEAD version.

  1. Remove default="US" from data.head.users.UserCreateRequest
  2. Add the following migration to versions.v2001_01_01:

    from cadwyn.structure import (
        VersionChange,
        schema,
        convert_request_to_next_version_for,
    )
    from data.head.users import UserCreateRequest, UserResource
    
    
    class MakeUserCountryRequired(VersionChange):
        description = 'Make user country required instead of the "USA" default'
        instructions_to_migrate_to_previous_version = (
            schema(UserCreateRequest).field("country").had(default="USA"),
        )
    
        @convert_request_to_next_version_for(UserCreateRequest)
        def add_default_value_to_country_field_in_request(request: RequestInfo):
            request.body["country"] = request.body.get("country", "USA")
    
  3. Add this migration into the version bundle:

    # versions/__init__.py
    
    from cadwyn.structure import Version, VersionBundle, HeadVersion
    from datetime import date
    from data import head
    from .v2001_01_01 import MakeUserCountryRequired
    
    version_bundle = VersionBundle(
        HeadVersion(),
        Version(date(2001, 1, 1), MakeUserCountryRequired),
        Version(date(2000, 1, 1)),
        head_schemas_package=head,
    )
    
  4. Regenerate the versioned schemas

That's it! Our old schemas will now contain a default but in HEAD country will be required. You might notice a weirdness: if we set a default in the old version, why would we also write a migration? That's because of a sad implementation detail of pydantic that prevents us from using defaults from old versions.

With incompatible default value in older versions

Let's say that we want to add a required field phone to our users. However, older versions did not have such a field at all. This means that the field is going to be nullable (or nonexistent) in the old versions but required in the HEAD version. This also means that older versions contain a wider type (str | None) than the HEAD version (str). So when we try to migrate request bodies from the older versions to HEAD -- we might receive a ValidationError because None is not an acceptable value for phone field in the new version. Whenever we have a problem like this, when older version contains more data or a wider type set of data, we can simply define a wider type in our HEAD version and then narrow it in latest.

So we will make phone nullable in HEAD, then make it required in latest, and then make it nullable again in older versions, thus making it possible to convert all of our requests to HEAD.

  1. Add phone field of type str | None to data.head.users.BaseUser
  2. Add phone field of type str | None with a default=None to data.head.users.UserResource because all users created with older versions of our API won't have phone numbers.
  3. Add the following migration to versions.v2001_01_01 which will make sure that phone is not nullable in 2001_01_01:

    from cadwyn.structure import VersionChange, schema
    from data.head.users import UserCreateRequest
    
    
    class MakePhoneNonNullableInLatest(VersionChange):
        description = (
            "Make sure the phone is nullable in the HEAD version to support "
            "versions older than 2001_01_01 where it became non-nullable"
        )
        instructions_to_migrate_to_previous_version = (
            schema(UserCreateRequest).field("phone").had(type=str),
            schema(UserCreateRequest).field("phone").didnt_have("default"),
        )
    
  4. Add the following version change to versions.v2001_01_01 (right under the version change above) which will make sure that phone is nullable in 2000_01_01:

    class AddPhoneToUser(VersionChange):
        description = (
            "Add a required phone field to User to allow us to do 2fa and to "
            "make it possible to verify new user accounts using an sms."
        )
        instructions_to_migrate_to_previous_version = (
            schema(UserCreateRequest)
            .field("phone")
            .had(type=str | None, default=None),
        )
    
  5. Add both migrations into our VersionBundle:

    # versions/__init__.py
    from cadwyn.structure import Version, VersionBundle, HeadVersion
    from datetime import date
    from data import head
    from .v2001_01_01 import MakePhoneNonNullableInLatest, AddPhoneToUser
    
    
    version_bundle = VersionBundle(
        HeadVersion(MakePhoneNonNullableInLatest),
        Version(date(2001, 1, 1), AddPhoneToUser),
        Version(date(2000, 1, 1)),
        head_schemas_package=head,
    )
    
  6. Regenerate the versioned schemas

See how we didn't remove the phone field from old versions? Instead, we allowed a nullable phone field to be passed into both old UserResource and old UserCreateRequest. This gives our users new functionality without needing to update their API version! It is one of the best parts of Cadwyn's approach: our users can get years worth of updates without switching their API version and without their integration getting broken.