|
| 1 | +--- |
| 2 | +id: input-unions |
| 3 | +title: Input Unions |
| 4 | +sidebar_label: Input Unions |
| 5 | +sidebar_position: 3 |
| 6 | +--- |
| 7 | + |
| 8 | +Input unions (Spec § [3.10.1](https://spec.graphql.org/September2025/#sec-OneOf-Input-Objects)) are a variant on standard input objects such that only one of the defined fields may be used in a query. This can be helpful if you have an input object, such as search parameters, that gives multiple ways to search, but you want the user submitting a query to choose exactly one option to search by. |
| 9 | + |
| 10 | +By definition an input union is a standard [input object](../types/input-objects.md) (a class or a struct), all rules of standard input objects apply (i.e. field names must be unique, no use of interfaces etc.). However... |
| 11 | + |
| 12 | +**Input unions:**<br/> |
| 13 | +✅ MUST have no default values declared on any field.<br/> |
| 14 | +✅ MUST have all nullable fields |
| 15 | + |
| 16 | +:::warning |
| 17 | +A declaration exception will be thrown and the server will fail to start if either of these rules are violated for any declared input union. |
| 18 | +::: |
| 19 | + |
| 20 | + |
| 21 | +## Creating An Input Union |
| 22 | +An object can be declared as an input union in multiple ways: |
| 23 | + |
| 24 | +### Using Attribution |
| 25 | + |
| 26 | +Use the `[OneOf]` attribute to mark an object as ALWAYS being an input union. Any place the class or struct is referenced as an input to a method it will be handled as an input union. |
| 27 | + |
| 28 | +```csharp title="Declaring an input union with the [OneOf] attribute" |
| 29 | +// highlight-next-line |
| 30 | +[OneOf] |
| 31 | +[GraphType(InputName = "SearchOptions")] |
| 32 | +public class SearchDonutParams |
| 33 | +{ |
| 34 | + public string Name {get; set;} |
| 35 | + public Flavor? Flavor {get; set; } // assume flavor is an enum |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +```csharp title="Using an Input Union in a Controller" |
| 40 | +public class BakeryController : GraphController |
| 41 | +{ |
| 42 | + [QueryRoot("findDonuts")] |
| 43 | + // highlight-next-line |
| 44 | + public List<Donut> FindDonuts(SearchDonutParams search) |
| 45 | + { |
| 46 | + /// guaranteed that only one value (name or flavor) is non-null |
| 47 | + return []; |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +```graphql title="Equivilant schema type definition" |
| 53 | +## relevant graphql type generated |
| 54 | +input SearchOptions @oneOf { |
| 55 | + name: String |
| 56 | + flavor: Flavor |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +### Inherit from GraphInputUnion |
| 61 | + |
| 62 | +Creating a class that inherits from `GraphInputUnion` works in the same way as using `[OneOf]` but adds some additional quality of life features in terms of metadata and default value handling. |
| 63 | + |
| 64 | +_See below for details on using `GraphInputUnion`_ |
| 65 | + |
| 66 | + |
| 67 | +```csharp title="Inheriting from GraphInputUnion" |
| 68 | +[GraphType(InputName "SearchParams")] |
| 69 | +// highlight-next-line |
| 70 | +public class SearchDonutParams : GraphInputUnion |
| 71 | +{ |
| 72 | + public string Name {get; set;} |
| 73 | + public Flavor? Flavor {get; set; } // assume flavor is an enum |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +```csharp title="Using an Input Union in a Controller" |
| 78 | +public class BakeryController : GraphController |
| 79 | +{ |
| 80 | + [QueryRoot("findDonuts")] |
| 81 | + // highlight-next-line |
| 82 | + public List<Donut> FindDonuts(SearchDonutParams search) |
| 83 | + { |
| 84 | + /// guaranteed that only one value (name or flavor) is non-null |
| 85 | + return []; |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +```graphql title="Equivilant schema type definition" |
| 91 | +## relevant graphql type generated |
| 92 | +input SearchOptions @oneOf { |
| 93 | + name: String |
| 94 | + flavor: Flavor |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +## Nullable Fields |
| 99 | +The specification defines an input union as *"a special variant of Input Object where exactly one field must be set and non-null, all others being omitted."* (Spec § [3.10.1](https://spec.graphql.org/September2025/#sec-OneOf-Input-Objects)). As such, all properties declared on a class or struct that is being used as an input union must be nullable, the supplied query MUST set exactly one field to a non-null value on a query document. |
| 100 | + |
| 101 | +```csharp title="Example Scenarios" |
| 102 | + |
| 103 | +// 🧨 FAIL: Flavor is non-nullable. A graph declaration exception will be thrown at start up. |
| 104 | +[OneOf] |
| 105 | +public class SearchDonutParams |
| 106 | +{ |
| 107 | + public string Name {get; set;} |
| 108 | + public Flavor Flavor {get; set; } // assume flavor is an enum |
| 109 | +} |
| 110 | + |
| 111 | +// 🧨 FAIL: Name declares a default value. A graph declaration exception will be thrown at start up. |
| 112 | +[OneOf] |
| 113 | +public class SearchDonutParams |
| 114 | +{ |
| 115 | + public SearchDonutParams |
| 116 | + { |
| 117 | + this.Name = "%"; |
| 118 | + } |
| 119 | + |
| 120 | + public string Name {get; set;} |
| 121 | + public Flavor? Flavor {get; set; } // assume flavor is an enum |
| 122 | +} |
| 123 | + |
| 124 | +// ✅ SUCCESS |
| 125 | +[OneOf] |
| 126 | +public class SearchDonutParams |
| 127 | +{ |
| 128 | + public string Name {get; set;} |
| 129 | + public Flavor? Flavor {get; set; } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +## Using `GraphInputUnion` |
| 134 | +This special base type can be used to expose some additional, quality of life methods for dealing with nullability and default values. |
| 135 | + |
| 136 | +```csharp |
| 137 | +public abstract class GraphInputUnion |
| 138 | +{ |
| 139 | + // Will return the value, if it was supplied on the query, otherwise fallbackValue. |
| 140 | + // this method is is heavily optimized to be performant at runtime |
| 141 | + public TReturn ValueOrDefault<TValue, TReturn>(Expression<Func<TObject, TValue>> selector, TReturn fallbackValue = default); |
| 142 | +} |
| 143 | + |
| 144 | +[GraphType(InputName = "SearchParams")] |
| 145 | +public class SearchDonutParams : GraphInputUnion |
| 146 | +{ |
| 147 | + public string Name {get; set;} |
| 148 | + |
| 149 | + public Flavor? Flavor {get; set; } // assume flavor is an enum |
| 150 | +} |
| 151 | + |
| 152 | + |
| 153 | +// Sample Usage |
| 154 | +public class BakeryController : GraphController |
| 155 | +{ |
| 156 | + [QueryRoot("findDonuts")] |
| 157 | + public List<Donut> FindDonuts(SearchDonutParams search) |
| 158 | + { |
| 159 | + InternalSearchParams internalParams = new(); |
| 160 | + internalParams.Name = search.ValueOrDefault(x => x.Name, "%"); |
| 161 | + internalParams.Flavor = search.ValueOrDefault(x => x.Flavor, Flavor.All); |
| 162 | + return _service.SearchDonuts(internalParams); |
| 163 | + } |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +:::info |
| 168 | +The `ValueOrDefault()` method will return a type of the fallback value, NOT of the input object property. This allows you to return non-null defaults in place of nullable values that must be passed on the input object. This should greatly reduce bloat in transferring query supplied values and reasonable fallbacks when necessary. When returning non-reference types, they must have compatibility between the nullable and non-nullable versions (e.g. `int` and `int?`) |
| 169 | +::: |
0 commit comments