Backwards compatible fieldname deprecation in GraphQL

Summary

Tatari Engineering, which transitioned to GraphQL for API development in 2018, introduced a concept called DeprecatedField to safely rename fields in GraphQL while maintaining backward compatibility. This homegrown extension, built on top of python’s graphene GraphQL library, allows Tatari to incrementally deploy field name changes in the backend without breaking existing frontend code, providing flexibility and stability to their API.

Source code for DeprecatedField can be located in this gist

Introduction to GraphQL Transition at Tatari

In 2018, Tatari made the decision to transition to GraphQL for our API development. This decision was driven by our desire to improve the efficiency and flexibility of our API development process, as well as to provide a better experience for our customers and stakeholders. Since then, our team has been using GraphQL to power our APIs and we have seen significant improvements in our development workflow and API performance.

Challenges of Renaming Fields in GraphQL

In a fast growing startup that is continually reacting to the market and business requirements, change is a constant. An essential part of building and maintaining APIs is ensuring that the business domain model satisfies the requirements. Renaming fields is an essential part of building APIs. It helps to ensure that the API is consistent with an evolving business domain model and satisfies the requirements of the API consumers. However, renaming fields can have a significant impact on the client code that consumes the API. It can break the existing code that relies on the old field names. Therefore, we needed a way to rename fields without breaking the client code. There is no built-in mechanism to rename a field in the GraphQL spec (although there is a spec to mark a field as deprecated). At Tatari, we introduced a concept we call DeprecatedField, which is a homegrown extension we have built on top of python’s graphene GraphQL library, which enables us to rename fields safely in GraphQL with backwards compatibility. This gives us the flexibility to maintain interoperability between the frontend and backend, and deliver these changes incrementally over time without having to coordinate a large synchronous release across the organization.

A Product Use Case that Required an Engineering Solution: Extending the “Campaign” Concept in 2023

In 2023, we broadened the interpretation of the term “campaign” on our platform, now representing more than individual inventory items by including entire collections, along with an advertiser’s goals and objectives. The challenge emerged from the persistence of our prior definition of “campaign,” which clashed with the introduction of the new definition.

Introducing DeprecatedField Concept

Our API would have become inconsistent in our use of “campaign” unless we renamed several fields related to the old usage of “campaign” across the entire GraphQL schema. The frontend code had several GraphQL queries that were referencing “campaign” from several different objects in our GraphQL schema. Renaming field names in GraphQL is a tedious and error-prone task that could easily break our frontend application without careful planning and coordination. However, with DeprecatedField, we were able to rename several fields and deploy those changes in isolation in our backend, without having to make parallel changes on the frontend.

DeprecatedField is a piece of software that is compatible with the graphene library that allows us to rename fields on ObjectType and InputObjectType safely. It provides a way to map an old field name to a new field name while preserving the existing field’s type, arguments, and resolver. By using DeprecatedField, we can ensure that the client code that relies on the old field name continues to work as expected.

Usage of DeprecatedField in Practice

To use DeprecatedField, we need to define a list of DeprecatedField objects for each ObjectType or InputObjectType that we want to rename. Each DeprecatedField object has two attributes: old_name and new_name. The old_name attribute specifies the old field name, and the new_name attribute specifies the new field name. We can define the DeprecatedField list as a class decorator on the ObjectType or InputObjectType class.

For example, suppose we have an ObjectType called Campaign that has two fields, id and name. You want to rename the name field to title. Without deprecate_gql, we would have had to do something like this:

class Campaign(graphene.ObjectType):
    id = graphene.Int(required=True)
    title = graphene.String()
    name = graphene.String(deprecation_reason=’replaced by title’)

    def resolve_title(self, info):
        return self.title
    def resolve_name(self, info):  # Notice that we have a resolver that points to the new field name
        return self.title

Here’s how you can define the Campaign class using DeprecatedField:


@deprecate_gql(
    [
        DeprecatedField(old_name='name', new_name='title'),
    ]
)
class Campaign(graphene.ObjectType):
    id = graphene.Int(required=True)
    title = graphene.String(required=True)

    def resolve_title(self, info):
        return self.title

In the example above, we defined the deprecate_gql decorator as a list of DeprecatedField objects. The DeprecatedField object specifies that the old field name is name, and the new field name is title. When the client queries the Campaign object, they can use either name or title as the field name, and both will work as expected.

You can also use DeprecatedField to rename fields on InputObjectType. For example, suppose you have an InputObjectType called CreateCampaignInput that has two fields, name and budget. You want to rename the name field to title. Here’s how you can define the CreateCampaignInput class using DeprecatedField:

@deprecate_gql(
    [
        DeprecatedField(old_name='name', new_name='title'),
    ]
)
class CreateCampaignInput(graphene.InputObjectType):
    budget = graphene.Int()
    title = graphene.String()

class CreateCampaignMutation(graphene.Mutation):
    class Arguments:
        input_data = CreateCampaignInput(required=True)

    campaign = graphene.Field(Campaign)

    def mutate(self, info, input_data):
        # Extract data from input_data
        budget = input_data.budget
        title = input_data.title

        # Create a new Campaign object with the extracted data
        new_campaign = Campaign(budget=budget, title=title)

        # Save the new Campaign object to the database
        new_campaign.save()

        # Return the new Campaign object
        return CreateCampaignMutation(campaign=new_campaign)

Without DeprecatedField, we would have had to modify our mutate code to do something like this:

title = input_data.title or input_data.name

Benefits of Using DeprecatedField

Encapsulating Code Changes in a decorator

The DeprecatedField feature allows us to encapsulate field changes using a Python decorator. When we eventually need to eliminate the old field name from the GraphQL schema, there’s no need to search for conditional expressions (e.g., input_data.title or input_data.name). We can just delete the decorator. The original developer responsible for the field replacement can easily identify these references during the initial work. Later, when it’s time to remove the field from the schema, another developer can effortlessly do so without the need to locate specific points in the code—simply removing the decorator accomplishes this task.

Gradual Deployment and Flexibility

The DeprecatedField class allows Tatari developers to easily modify and customize the fields of our GraphQL schema. By using DeprecatedField, our backend developers can safely and confidently make these changes without affecting our existing front end applications. The front end engineers can gradually roll out the new field names at their own pace. This can lead to a more flexible and stable API that is easier to maintain and evolve over time.

Michael Librodo

Michael is a Staff engineer at Tatari.