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.
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.