Skip to content

Inputs and Nullability

The last building block is the data that flows into your schema. Object types describe what comes back; input object types describe what clients send. Combined with nullability, lists, and default values, inputs are how you design arguments that are both expressive and hard to get wrong. This lesson closes the module on the side of the contract that clients write to.

When a field takes more than a couple of arguments, listing them all inline gets unwieldy. An input object type bundles related arguments into a single named structure. It looks like an object type but is declared with the input keyword and may only contain scalars, enums, lists, and other inputs — never object types:

input CreateBookInput {
title: String!
authorId: ID!
pageCount: Int
}
type Mutation {
createBook(input: CreateBookInput!): Book
}

Now createBook takes one argument, input, instead of three loose ones. This is the standard pattern for mutations: a single input argument keeps the mutation signature stable even as the fields inside it evolve. Inputs and object types are deliberately separate kinds — an object type is for output, an input type is for input, and GraphQL will not let you mix them.

Every type reference is nullable by default; a trailing ! makes it non-null. On an input field, ! means the client must provide the value:

input CreateBookInput {
title: String! # required — the client must send it
authorId: ID! # required
pageCount: Int # optional — may be omitted or sent as null
}

title and authorId are non-null, so omitting them is a validation error caught before any resolver runs. pageCount is nullable, so it is optional. Nullability on inputs is how you express “required vs optional” without writing a line of validation code — the type system enforces it for you.

A list type is written with square brackets: [Book]. Because both the list and its elements can independently be null, list types often carry two exclamation marks doing two different jobs:

input AddTagsInput {
bookId: ID!
tags: [String!]!
}

Decode [String!]! from the inside out:

  • The inner String! means every element of the list is a non-null string — no null holes inside.
  • The outer ! means the list itself is never null — though it may still be empty, [].

So tags is always a list, and every tag in it is a real string. The same reading applies to output fields like [Book!]!. Choosing the right combination of marks tells clients exactly what to expect: must they handle a missing list? a list with gaps? [String!]! answers no to both.

flowchart TD
  Mutation["Mutation.createBook(input)"]
  Input["input CreateBookInput"]
  Required["title: String! (required)"]
  Required2["authorId: ID! (required)"]
  Optional["pageCount: Int (optional)"]
  List["tags: [String!]! (non-null list of non-null)"]
  Mutation --> Input
  Input --> Required
  Input --> Required2
  Input --> Optional
  Input --> List
An input object bundles arguments; nullability and lists shape what is required.

An optional argument or input field can declare a default value with =. If the client omits it, the default is used:

type Query {
books(first: Int = 10, sortBy: String = "title"): [Book!]!
}

If a client calls books with no arguments, it gets the first 10 books sorted by title. Defaults let you keep arguments optional while still giving resolvers a sensible value to work with — the resolver never sees undefined for first, it sees 10. A field with a default is, by definition, optional, so do not also mark it non-null in a way that contradicts the default.

A few habits keep inputs pleasant over time. Wrap mutation arguments in a single input type so the signature is stable. Make a field non-null only when the operation truly cannot proceed without it — over-using ! makes an API rigid and breaks clients when requirements loosen. Provide defaults for pagination and sorting so the simple call is the easy call. And keep input types focused: one input per operation, named after the operation, beats a giant shared bag of optional fields.

Inputs, nullability, and defaults, run for real

Section titled “Inputs, nullability, and defaults, run for real”

The example below defines a CreateBookInput, a mutation that consumes it, and runs the mutation with a partial input — relying on nullability to allow the optional field to be omitted. Press Run.

JavaScript

The mutation sent only the two required fields. Because pageCount was nullable in the input, omitting it was legal, and the resolver filled it with null. The tags field came back as [] — a non-null list, just empty — exactly as [String!]! promised. The type system validated the whole input before the resolver ever ran.

What is an input object type used for?
On an input field, what does a trailing ! mean?
What does the type [String!]! describe?
What does a default value (e.g. first: Int = 10) achieve?