|
| 1 | ++++ |
| 2 | +title = "A comprehensive guide to GraphQL API changes" |
| 3 | +author = "Andreas Marek" |
| 4 | +tags = [] |
| 5 | +categories = [] |
| 6 | +date = 2022-02-07T00:00:00+10:00 |
| 7 | +toc = "true" |
| 8 | ++++ |
| 9 | + |
| 10 | +__Note: this guide aims to be an update document which will be updated if needed.__ |
| 11 | + |
| 12 | +# Breaking changes |
| 13 | + |
| 14 | +They type system of GraphQL gives you confidence what the shape of your API is: |
| 15 | +which data to expect from each field, which field can be null or not etc. |
| 16 | +This is great, but an API is rarely absolutely fixed, but rather continues to evolve over time. |
| 17 | + |
| 18 | +The beauty of the type system is that not every change is equal, but some are perfectly fine |
| 19 | +and others you might want to try to avoid. The most obvious dual classification distinguishes between |
| 20 | +a breaking change and a non-breaking change. While this makes sense at first glance, it is to simplistic |
| 21 | +to cover all differente changes that can happen. |
| 22 | + |
| 23 | +We will look at the existing definitions and then classify all different changes in detail. |
| 24 | + |
| 25 | +# GraphQL Spec definition of breaking |
| 26 | +The [GraphQL spec](https://spec.graphql.org/draft/#sec-Validation.Type-system-evolution) says the following: |
| 27 | + |
| 28 | +> Any change that can cause a previously valid request to become invalid is considered a breaking change. |
| 29 | +
|
| 30 | +Every GraphQL request is either considered valid or invalid. If a request is invalid it will not be executed. |
| 31 | +This definition of breaking change would cover the following change: |
| 32 | + |
| 33 | +Original schema: |
| 34 | +{{< highlight TypeScript "linenos=table" >}} |
| 35 | +type Query { |
| 36 | + name:String |
| 37 | +} |
| 38 | +{{< / highlight >}} |
| 39 | +<p/> |
| 40 | +New Schema: |
| 41 | +{{< highlight TypeScript "linenos=table" >}} |
| 42 | +type Query { |
| 43 | + newName:String |
| 44 | +} |
| 45 | +{{< / highlight >}} |
| 46 | +<p/> |
| 47 | + |
| 48 | +Now the previously valid query `{name}` would become invalid because there is no top-level field `name` anymore. |
| 49 | +This is clearly a breaking change. |
| 50 | + |
| 51 | +But what about this: |
| 52 | + |
| 53 | +Original schema: |
| 54 | +{{< highlight TypeScript "linenos=table" >}} |
| 55 | +type Query { |
| 56 | + name:String! |
| 57 | +} |
| 58 | +{{< / highlight >}} |
| 59 | +<p/> |
| 60 | +New Schema: |
| 61 | +{{< highlight TypeScript "linenos=table" >}} |
| 62 | +type Query { |
| 63 | + name:String |
| 64 | +} |
| 65 | +{{< / highlight >}} |
| 66 | +<p/> |
| 67 | + |
| 68 | +Here the type of `name` was changed from non-nullable to nullable: `String!` => `String`. |
| 69 | +Now `{name}` is still a valid query which would be executed without error. In general this is also |
| 70 | +considered a breaking change, because the old schema assured that client that `name` could never be null, |
| 71 | +but now it can. Every change like this where the type system guarantees less, is considered a breaking |
| 72 | +change. |
| 73 | + |
| 74 | +This example shows that the definition of breaking change in the spec doesn't cover all the cases |
| 75 | +we are interested in. |
| 76 | + |
| 77 | +# GraphQL.js definition of breaking change |
| 78 | +The next good thing after the spec is the JavaScript reference implementation. |
| 79 | +GraphQL.js [contains a function](https://github.com/graphql/graphql-js/blob/main/src/utilities/findBreakingChanges.js) |
| 80 | +`findBreakingChanges` which compares an old and a new schema and returns |
| 81 | +a list of breaking and dangerous changes. |
| 82 | + |
| 83 | +Unfortunately the functions code itself doesn't make clear what breaking or dangerous means exactly, but |
| 84 | +the discussion in the two issues [#992](https://github.com/graphql/graphql-js/pull/992) and |
| 85 | +[#968](https://github.com/graphql/graphql-js/issues/968) shed some light on it: |
| 86 | + |
| 87 | +>The above is with the assumption that "dangerous change" means |
| 88 | +>"it could affect your client if you built things poorly (i.e. didn't provide a default in a switch statement), |
| 89 | +>but won't affect clients built with the concept of GraphQL breaking changes in mind". |
| 90 | +> |
| 91 | +>Basically: if something breaks due to a "dangerous change" there's some underlying, |
| 92 | +>root issue on your client you should dig in and fix so future changes don't cause |
| 93 | +>that problem. But not coding defensively can lead to these issues. Whereas if you make a |
| 94 | +>"breaking change" it's expected that clients won't be able to handle it correctly. |
| 95 | +
|
| 96 | +<p/> |
| 97 | + |
| 98 | +>When building clients, it's best to be defensive against possible future expansions and handle |
| 99 | +>those cases. For enums, we typically include an else or switch default clause when branching on |
| 100 | +>them to handle cases the client doesn't know about. If your clients aren't programming defensively |
| 101 | +>like this, then it's true that expanding the possible response values could cause issues for those |
| 102 | +>clients - it's not an entirely safe change. |
| 103 | +
|
| 104 | +This basically means that you take client side development considerations into account and |
| 105 | +not only the guarantees of the type system itself. |
| 106 | + |
| 107 | +For example as described above: adding a value to an Enum is a non breaking change |
| 108 | +if you just look at the schema itself, |
| 109 | +but it could lead to problems if the clients are not developed with the possibilities |
| 110 | +of new Enum values in mind. |
| 111 | + |
| 112 | +And while the list provided by GraphQL.js is much more detailed than the short |
| 113 | +definition in the spec, the nuances are not really obvious, it is only available as code |
| 114 | +and sometimes it could be more specific. |
| 115 | + |
| 116 | +For example removing a Directive is not always a breaking change if the Directive |
| 117 | +is never used at a Query element or adding an argument which is required is not breaking |
| 118 | +if the argument has a default value. |
| 119 | + |
| 120 | +# Comprehensive list of changes with explanation |
| 121 | + |
| 122 | +This sections aims to provide a comprehensive list of changes and |
| 123 | +what consequence each change have. |
| 124 | + |
| 125 | +Same naming conventions: |
| 126 | + |
| 127 | + |
| 128 | +- the type of a type is called kind. It can be Enum, |
| 129 | +Scalar, Input Object, Object, Interface or Union. |
| 130 | +- Argument means arguments for fields and for Directives if |
| 131 | +not explicitly mentioned otherwise. |
| 132 | +- Query location means a location for a Directive in a Query (eg. field) |
| 133 | +- Schema location means a location for a Directive in SDL (eg. field definition) |
| 134 | +- Query Directive is a Directive which is a valid for locations in a Query |
| 135 | +- Schema Directive is a Directive which is a valid for locations in a SDL |
| 136 | +- Input types are Scalar, Enum, Input Objects. |
| 137 | +- Output types are Scalar, Enum, Object, Interfaces and Union. |
| 138 | + |
| 139 | +List of changes: |
| 140 | + |
| 141 | +## 1. A type is removed. |
| 142 | +When a type is removed every Query which directly uses the type by name becomes invalid. |
| 143 | +For input types this means a variable declaration becomes invalid: |
| 144 | + |
| 145 | +{{< highlight Scala "linenos=table" >}} |
| 146 | +query($var: InputTypeWhichIsRemoved) { |
| 147 | +# more |
| 148 | +} |
| 149 | +{{< / highlight >}} |
| 150 | + |
| 151 | +For composite output types every query which uses the type as type condition becomes invalid: |
| 152 | +{{< highlight Scala "linenos=table" >}} |
| 153 | +{ |
| 154 | + ... on TypeWhichIsRemoved { |
| 155 | + } |
| 156 | +} |
| 157 | +{{< / highlight >}} |
| 158 | + |
| 159 | +When a Scalar or Enum which is used as output type is removed it means the field |
| 160 | +which returns this Scalar or Enum is either removed or changed in a breaking way. |
| 161 | + |
| 162 | +## 2. The kind of a type is changed. |
| 163 | + |
| 164 | +Changing the kind of a type means fundamentally changing the guarantee about this type. |
| 165 | + |
| 166 | +Open discussion: changing an Object (which was not used as type in an Union) |
| 167 | +is maybe not breaking? |
| 168 | + |
| 169 | +## 3. A type of an Union is removed. |
| 170 | + |
| 171 | +Every request which queried the type via Fragment becomes invalid. |
| 172 | +{{< highlight Scala "linenos=table" >}} |
| 173 | +{ unionField { |
| 174 | + ... on TypeWhichIsRemoved { |
| 175 | + # more |
| 176 | + } |
| 177 | + } |
| 178 | +} |
| 179 | +{{< / highlight >}} |
| 180 | + |
| 181 | +## 4. A value is removed from an Enum which is used as input. |
| 182 | + |
| 183 | +Every request which used the value becomes invalid. |
| 184 | + |
| 185 | +## 5. A required input field or argument is added which doesn't have a default value. |
| 186 | + |
| 187 | +Every request which queried the field becomes invalid because the required |
| 188 | +argument or input field is not provided. |
| 189 | + |
| 190 | +## 6. An Interface is removed from an Object or Interface. |
| 191 | + |
| 192 | +Every request which queried the type via Fragment becomes invalid. |
| 193 | + |
| 194 | +## 7. An argument or input field is removed. |
| 195 | + |
| 196 | +Every request which provided the argument or input field becomes invalid. |
| 197 | + |
| 198 | +## 8. An argument type or input field is changed in an incompatible way. |
| 199 | + |
| 200 | +Any change which is not just removing non-nullable constraints is breaking. |
| 201 | + |
| 202 | +TODO: more explanation. |
| 203 | + |
| 204 | +## 9. A Query Directive was removed. |
| 205 | + |
| 206 | +Every request which used the Directive becomes invalid. |
| 207 | + |
| 208 | +## 10. A Query Directive was changed from repeatable to not repeatable. |
| 209 | + |
| 210 | +Every request which provided multiple instances of the Directive on the same element |
| 211 | +becomes invalid. |
| 212 | + |
| 213 | +## 11. A Query location for a Query Directive was removed. |
| 214 | + |
| 215 | +Every request which has the Directive on the now removed location becomes invalid. |
| 216 | + |
| 217 | +## 12. A value is added to an Enum. |
| 218 | + |
| 219 | +If a client is not developed in defensive way which expects new Enum values |
| 220 | +it can cause problems. |
| 221 | + |
| 222 | +## 13. Default value for argument or input field is changed |
| 223 | + |
| 224 | +Every request which didn't provide any value for this argument |
| 225 | +or input field is now using the new default value. |
| 226 | + |
| 227 | +# Characteristics of each change |
| 228 | + |
| 229 | +As discussed above there are different aspects to a change we should consider: |
| 230 | + |
| 231 | +- Will it make previously valid queries invalid |
| 232 | +- Does it weaken or fundamentally changes the guarantees of the schema |
| 233 | +- Could it be problematic for clients |
| 234 | + |
| 235 | +| Change | Causes invalid queries | Less/incompatible guarantees | Maybe problematic for clients | |
| 236 | +|--------------------------------------|------------------------|------------------------------|-------------------------------| |
| 237 | +| #1 Type removed | yes | na | na | |
| 238 | +| #2 Kind changed | yes | yes | na | |
| 239 | +| #3 Union type removed | yes | no | na | |
| 240 | +| #4 Input Enum value removed | yes | no | na | |
| 241 | +| #5 Required input added | yes | | | |
| 242 | +| #6 Interface removed | yes | | | |
| 243 | +| #7 Argument/Input field removed | yes | | | |
| 244 | +| #8 Argument/Input field type changed | yes | | | |
| 245 | +| #9 Query directive removed | yes | | | |
| 246 | +| #10 Query directive non-repeatable | yes | | | |
| 247 | +| #11 Query directive location removed | yes | | | |
| 248 | +| #12 Value added to Enum | no | no | yes | |
| 249 | +| #13 Default value changed | no | no | yes | |
| 250 | + |
| 251 | + |
| 252 | +# Feedback or questions |
| 253 | +We use [GitHub Discussions](https://github.com/graphql-java/graphql-java/discussions) for general feedback and questions. |
| 254 | + |
| 255 | +You can also checkout our [Workshops](/workshops) for more possibilities to learn GraphQL Java. |
| 256 | + |
| 257 | + |
| 258 | + |
| 259 | + |
0 commit comments