Input Objects
INPUT_OBJECT
graph types (a.k.a. input objects) represent complex data supplied to arguments on fields or directives. Anytime you want to pass more data than a single string or a number, perhaps an Address or a new Employee record, you use an INPUT_OBJECT to represent that entity in GraphQL. When the system scans your controllers, if it comes across a class or struct used as a parameter to a method it will attempt to generate the appropriate input type definition to represent that class.
The rules surrounding naming, field declarations, exclusions, use of [GraphSkip]
etc. apply to input objects but with a few key differences:
- Unless overridden, an input object is named the same as its class name, prefixed with
Input_
(e.g.Input_Address
,Input_Employee
) - Only public properties with a
get
andset
will be included.- Property return types cannot be
Task<T>
, aninterface
and cannot implementIGraphUnionProxy
orIGraphActionResult
. Such properties are always skipped.
- Property return types cannot be
- Methods are always skipped.
Customized Type Names
Input objects can be given customized names, just like with object types, using the [GraphType]
attribute.
[GraphType(InputName = "NewDonutModel")]
public class Donut
{
public int Id { get; set; }
public string Name { get; set; }
public DonutType Type { get; set; }
public decimal Price { get; set; }
}
input NewDonutModel {
id: Int! = 0
name: String = null
type: DonutType! = FROSTED
price: Decimal! = 0
}
Note the specific callout to
InputName
in the attribution.
Use an Empty Constructor
When GraphQL executes a query it will attempt to create an instance of your input object then assign the key/value pairs received on the query to the properties. In order to do the initial instantiation it requires a public parameterless constructor to do so.
public class BakeryController : GraphController
{
[Mutation("createDonut")]
public bool CreateNewDonut(DonutModel donut)
{/*....*/}
}
// DonutModel.cs
public class DonutModel
{
//Use a public parameterless constructor
public DonutModel()
{
}
public int Id { get; set; }
public string Name { get; set; }
}
Because of this consturctor restriction it can be helpful to separate your classes between "input" and "output" types much is the same way we do with
ViewModel
vs.BindingModel
objects with REST queries in ASP.NET. This is optional, mix and match as needed by your use case.
Properties Must Have a Public Setter
Properties without a setter are ignored. At runtime, GraphQL compiles an expression tree with the set assignments declared on the graph type, it won't attempt to sneakily reflect and invoke a private or protected setter.
public class Donut
{
public int Id { get; }
public string Name { get; set; }
public DonutType Type { get; set; }
public decimal Price { get; set; }
}
# Id field is not included
input Input_Donut {
name: String = null
type: DonutType! = FROSTED
price: Decimal! = 0
}
Methods are Ignored
While its possible to have methods be exposed as resolvable fields on regular OBJECT
types, they are ignored for input types regardless of the declaration rules applied to the type.
public class Donut
{
[GraphField("salesTax")]
public decimal CalculateSalesTax(decimal taxPercentage)
{
return this.Price * taxPercentage;
}
public int Id { get; set; }
public string Name { get; set; }
public DonutType Type { get; set; }
public decimal Price { get; set; }
}
# CalculateSalesTax is not included
input Input_Donut {
id: Int! = 0
name: String = null
type: DonutType! = FROSTED
price: Decimal! = 0
}
Non-Nullability
By default, all properties that are reference types (i.e. classes) are nullable and all value types (primatives, structs etc.) are non-nullable
public class Donut
{
public Recipe Recipe { get; set; } // reference type
public int Quantity { get; set; } // value type
}
input Input_Donut {
recipe: Input_Recipe = null # nullable
quantity: Int! = 0 # not nullable
}
If you want to force a reference type to be non-null you can use the [GraphField]
attribute to augment the field's type expression.
public class Donut
{
public Donut()
{
// we must supply a non-null default value
// for a non-nullable field
this.Recipe = new Recipe("Flour, Sugar, Salt");
}
[GraphField(TypeExpression = "Type!")]
public Recipe Recipe { get; set; } // reference type
public int Quantity { get; set; } // value type
}
input Input_Donut {
recipe: Input_Recipe! = {ingredients : "Flour, Sugar, Salt" }
quantity: Int! = 0
}
We assigned a recipe in the class's constructor to use as the default value.
Any non-nullable field, that does not have the [Required]
attribute (see below), MUST have a default value assigned to it that is not null
. A GraphTypeDeclarationException
will be thrown at startup if this is not the case.
Required Fields And Default Values
Add [Required]
(from System.ComponentModel) to any property to force a user to supply the field in a query document.
Any non-required field will automatically be assigned a default value if not supplied on a query. This default value is equivilant to the property value of the object when its instantiated via its public, parameterless constructor.
public class Donut
{
public Donut()
{
// set custom defaults if needed
this.Type = DonutType.Frosted;
this.IsAvailable = true;
}
[Required]
public int Id { get; set; }
public string Name { get; set; }
public DonutType Type { get; set; }
public Bakery Bakery { get;set; }
public bool IsAvailable { get; set; }
public int SkuNumber { get; set; }
}
input Input_Donut {
id: Int! # No Default Value on Id
name: String = null
type: DonutType! = FROSTED
bakery: Input_Bakery = null
isAvailable: Boolean! = true
skuNumber: Int! = 0
}
Nullable Fields are Never Required
By Definition (spec § 5.6.4), a nullable field without a
declared default value is still considered optional and does not need to be supplied. That is to say that if a query does not include a field that is nullable, it will default to null
regardless of the use of the [Required]
attribute.
These two property declarations for an input object are identical as far as graphql is concerned:
public InputEmployee
{
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
Both FirstName
and LastName
are of type string
, which is nullable. GraphQL will ignore the required attribute and still allow a query to process even if neither value is supplied on a query.
Required Reference Types
Combine the [Required] attribute with a non-nullable type expression to force an otherwise nullable field to be required.
public class Bakery
{
[Required]
[GraphField(TypeExpression = "Type!")]
public Person Owner { get; set; }
}
# No Default Value is supplied.
# the 'owner' must be supplied on a query
input Input_Bakery {
owner: Input_Person!
}
Owner
is of type Person, which is a reference type, which is nullable by default. By augmenting its type expression to be non-null and adding the [Required]
attribute graphql will not supply a default value require it to be supplied on a query.
Enum Fields and Coercability
Any default value declared for an input field must be coercible by its target graph type in the target schema. Because of this there is a small got'cha situation with enum values.
Take a look at this example of an enum and input object:
public class Donut
{
public string Name { get; set; }
public DonutFlavor Flavor { get; set; }
}
public enum DonutFlavor
{
[GraphSkip]
Vanilla = 0,
Chocolate = 1,
}
When Donut
is instantiated the value of Flavor will be Vanilla
because
thats the default value (0) of the enum. However, the enum value Vanilla
is marked as being skipped in the schema.
Because of this mismatch, a GraphTypeDeclarationException
will be thrown when the introspection data for your schema is built. As a result, the server will fail to start until the problem is corrected.
You can get around this by setting an included enum value in the consturctor:
public class Donut
{
public Donut()
{
// set the value of flavor to an enum value
// included in the graph
this.Flavor = DonutFlavor.Chocolate;
}
public string Name { get; set; }
public DonutFlavor Flavor { get; set; }
}
Enum values used for the default value of input object properties MUST also exist as values in the schema or an exception will be thrown.