5
votes

I have the following graphql schema definition in production today:

type BasketPrice {
  amount: Int!
  currency: String!
}

type BasketItem {
   id: ID!
   price: BasketPrice!
}

type Basket {
   id: ID!
   items: [BasketItem!]!
   total: BasketPrice!
}

type Query {
   basket(id: String!): Basket!
}

I'd like to rename BasketPrice to just Price, however doing so would be a breaking change to the schema because clients may be referencing it in a fragment, e.g.

fragment Price on BasketPrice {
   amount
   currency
}

query Basket {
   basket(id: "123") {
      items {
         price {
            ...Price
         }
      }
      total {
         ...Price
      }
   }
}

I had hoped it would be possible to alias it for backwards compatibility, e.g.

type Price {
  amount: Int!
  currency: String!
}

# Remove after next release.
type alias BasketPrice = Price;

type BasketPrice {
  amount: Int!
  currency: String!
}

type BasketItem {
   id: ID!
   price: BasketPrice!
}

type Basket {
   id: ID!
   items: [BasketItem!]!
   total: BasketPrice!
}

type Query {
   basket(id: String!): Basket!
}

But this doesn't appear to be a feature. Is there a recommended way to safely rename a type in graphql without causing a breaking change?

2

2 Answers

1
votes

There's no way to rename a type without it being a breaking change for the reasons you already specified. Renaming a type is a superficial change, not a functional one, so there's no practical reason to do this.

The best way to handle any breaking change to a schema is to expose the new schema on a different endpoint and then transition the clients to using the new endpoint, effectively implementing versioning for your API.

The only other way I can think of getting around this issue is to create new fields for any fields that utilize the old type, for example:

type BasketItem {
   id: ID!
   price: BasketPrice! @ deprecated(reason: "Use itemPrice instead")
   itemPrice: Price!
}

type Basket {
   id: ID!
   items: [BasketItem!]!
   total: BasketPrice! @ deprecated(reason: "Use basketTotal instead")
   basketTotal: Price!
}
0
votes

I want this too, and apparently we can't have it. Making sure names reflect actual semantics over time is very important for ongoing projects -- it's a very important part of documentation!

The best way I've found to do this is multi-step, and fairly labor intensive, but at least can keep compatibility until a later time. It involves making input fields optional at the protocol level, and enforcing the application-level needs of having "one of them" at the application level. (Because we don't have unions.)

input OldThing {
   thingId: ID!
}

input Referee {
  oldThing: OldThing!
}

Change it to something like this:

input OldThing {
   thingId: ID!
}

input NewThing {
  newId: ID!
}

input Referee {
  oldThing: OldThing @ deprecated(reason: "Use newThing instead")
  newThing: NewThing
}

In practice, all old clients will keep working. You can update your handler code to always generate a NewThing, and then use a procedural field resolver to copy it into oldThing if asked-for (depending on which framework you're using.) On input, you can update the handler to always translate old to new on receipt, and only use the new one in the code. You'll also have to return an error manually if neither of the elements are present.

At some point, clients will all be updated, and you can remove the deprecated version.