graphqlbackwards-compatibility

Is there a backwards compatible method for changing the return type of a GraphQL Mutation?


I have a GraphQL project where many of the mutations return a simple boolean for success (union'ed with an error type).

# An object wrapper for a un/successful mutation result
type MutationResult {
    # whether the mutation was successful
    success: Boolean!
}

# An indicator that a user-error
type FieldError {
    field: String!
    message: String!
    type: String
}

# A wrapper object around [FieldError] for use in interfaces and unions.
type FieldErrors {
    fieldErrors: [FieldError!]!
}

One such example is this addPhoneNumber mutation

extend type Mutation {
    # All Mutations that can be applied to a User
    user: UserMutation!
}

union AddPhoneNumberResult = FieldErrors | MutationResult

type UserMutation {
    # Add phone number to the user's account
    addPhoneNumber(phoneNumber: String!): AddPhoneNumberResult! @RequireAuthorization(secure: true)

# Several other mutations...

I would like to change the return type of this mutation to include more details about the result, like this.

union AddPhoneNumberResult = FieldErrors | PhoneNumber

type PhoneNumber {
    # A phone number associated with this account
    phoneNumber: String!

    # Whether or not this phone number has been verified via the SMS verification code
    verified: Boolean!

    factorStatus: FactorStatus!
}

enum FactorStatus {
    PENDING_ACTIVATION,
    ACTIVE,
    EXPIRED
}

However, it occurs to me that this is a breaking change for any client that is calling this mutation. The request body would necessarily change from.

mutation AddPhoneNumber {
    user {
        addPhoneNumber(phoneNumber: "8675309") {
            ... on FieldErrors {
                fieldErrors {
                    field
                    message
                }
            }

            ... on MutationResult {
                success
            }
        }
    }
}

to

mutation AddPhoneNumber {
    user {
        addPhoneNumber(phoneNumber: "8675309") {
            ... on FieldErrors {
                fieldErrors {
                    field
                    message
                }
            }

            ... on PhoneNumber {
                phoneNumber
                verified
                factorStatus
            }
        }
    }
}

the moment we deployed the change to the GraphQL service.

We can't change MutationResult itself because is used by many other mutations; changing it would essentially be adding a nullable field that no other mutation would fill out. Any other change I can think of to the response falls into the same trap.

The only backwards compatible way I can think make this change is to introduce an entirely new mutation and wire it up separately, leaving the addPhoneNumber mutation alone. All the advice I can find on backwards compatibility seems focused on queries where the system was designed with the foresight of granular types. I've not found a similar level of guide for mutations and mutation responses.

Is there something else I'm not seeing?

The server is a Java project using com.graphql-java, if that makes a difference in strategy.


Solution

  • You can refer to GitLab for a real world example for handling breaking changes in GraphQL.

    Basically the idea is that you need to do it step by step. First, make the AddPhoneNumberResult can support all the possible union members :

    union AddPhoneNumberResult = FieldErrors | MutationResult | PhoneNumber
    

    Then notify the API users that MutationResult will be sooner or later be removed. Give them appropriate time to update their codes to use the new union member. Otherwise , they may face a risk that their application will suddenly not work if server has some updates.

    In the server side , you can consider to monitor the usage of MutationResult and actually delete it when you feel comfortable its usage is low enough.

    P.S @deprecated allows to document the deprecated reasons for a field.Most GraphQL tools will detect it and give user warning if they try to use a deprecated field. But it only can apply on the field and enum value. There is a discussion before to make it also can be used with union member but I don't know its latest status. Just an FYI.