Skip to content
beginner Phase 2 · Schema Foundations: Types and the Query Root

Object Types and Fields

Defining object types with typed fields that form the shape of your API graph.

40m
1 problems
Topic Progress 0%

A Stronger Contract Than JSON

A Stronger Contract Than JSON

A REST handler returns an object and the client finds out what is in it at runtime. If the handler renames totalPrice to amount, every consumer breaks silently until someone reads the payload. GraphQL replaces that guesswork with a type system that lives on the server.

type Product {
  id: ID!
  title: String!
  priceCents: Int!
  inStock: Boolean!
}

Each line is a promise. id: ID! says the field always exists and serializes as a string. priceCents: Int! says the value is a signed 32-bit integer, which is narrower than JSON's number and therefore a stronger guarantee than "some number will be there." The exclamation mark is the non-null modifier, covered in detail in enums, lists, and non-null.

The guarantee is enforced, not documented. When a client asks for a field the type does not declare, validation rejects the request before execution:

Validation error: Cannot query field "price" on type "Product".
Did you mean "priceCents"?

No resolver ran, no query reached the database, and the client learns the correct spelling from the error message. In REST the same mistake returns a payload with a missing key that surfaces three screens later as undefined.

Because the contract is structured data, tools can read it. Introspection lets a workbench print every type and field, a linter enforce naming rules, and a generator produce typed client code; see introspection mechanics and TypeScript code generation. The schema stops being a wiki page and becomes the source both sides compile against.

Turning Rows Into a Graph

Turning Rows Into a Graph

Object types are useful alone; they become a graph when they reference each other.

type User {
  id: ID!
  displayName: String!
  orders: [Order!]!
}

type Order {
  id: ID!
  placedAt: String!
  buyer: User!
  items: [OrderItem!]!
}

Two structural facts follow, and they are the reason the technology carries the name it does.

References are just fields. User.orders and Order.buyer point at other object types, so a client can walk from a user to their orders and back to the buyer in one operation. REST would answer that with either embedded JSON that duplicates data or a second call the client has to discover and make.

Cycles are legal. User -> Order -> User is a valid schema, and so is a self-reference such as User.friends: [User!]. The type system describes shape, not traversal; preventing a client from looping forever is a separate concern handled by query depth limits rather than by forbidding the cycle in the schema.

A few rules keep the graph coherent:

  • Field names must be unique within a type; two title fields on one type are a schema error, not an overload.
  • Object-typed fields must declare their sub-selection in queries, while scalar and enum fields must not, so the shape of every request is unambiguous.
  • Arguments attach to fields, which is how a single field like orders(status: OrderStatus) serves many queries; see field arguments design for how to shape them.
  • Names beginning with a double underscore are reserved for introspection (__typename, __schema, __type). Declaring __internalId in your own schema collides with machinery the platform owns, so tools reject it.

The design question you will actually face is not syntax but granularity: which relationships become fields, and which stay behind an argument. Getting that wrong produces a schema where every query triggers a full traversal, and that is a performance decision dressed as a typing decision.

Validation Runs Before Any Resolver

Validation Runs Before Any Resolver

The pipeline behind a GraphQL request is parse, validate, execute. Object types are what make the middle step powerful, so it pays to know exactly what validation checks and what it deliberately does not.

What validation guarantees:

Check Failure you see
Field exists on the type Cannot query field "prcie" on type "Product"
Sub-selection present on object fields Field "orders" of type "[Order!]!" must have a selection of subfields
No sub-selection on leaf fields Field "title" must not have a selection of subfields
Arguments exist and match types Unknown argument "staust" on field "orders"
Same response key not selected twice with conflicting shapes Fields conflict because they return differing types

What validation cannot check: whether your resolvers return data of the declared type. The schema describes intent; the resolver is where intent meets the database. A priceCents: Int! resolver that returns a string produces an execution-time field error rather than a validation error, which is why the two categories must be kept separate when you debug.

Practical consequences for a team:

  • Reviewing a schema pull request reviews the API surface itself, because that text is what clients compile against.
  • Broken client queries fail in CI or in the workbench, not in production.
  • Empty selection sets are rejected: a query must ask for at least one field, so query { product(id: "1") { } } never reaches execution.

Interview angle: "a client requests a field that does not exist; what happens and how do you know?" Answer with the phase (validation), the absence of side effects (no resolver, no database call), and the tool that surfaces it (the same validation rules that run in CI and the workbench).

Practice Problems

0 / 1 solved
Design Object Types for a Support Desk
Schema modeling

Model a support desk as GraphQL object types. Requirements: a Ticket has an id, subject, status drawn from a fixed set (OPEN, PENDING, CLOSED), and a creator; a Customer has an id, email, and their tickets; an Agent has an id, name, and the tickets they handle; tickets carry a list of comments where each comment has body and author. Every list that a screen always renders must be non-null, and every id must be an ID. Deliver the SDL.

Example:

Input: Requirements: Ticket/Customer/Agent/Comment with two people referencing tickets and tickets carrying comments

Output: type Ticket { id: ID! status: OrderStatusLikeEnum! creator: Customer! assignee: Agent comments: [Comment!]! } plus the other three types

Each relationship becomes a field pointing at another object type, so a single query can walk ticket -> creator -> tickets without extra round trips.

Optimal Solution — O(n) field validations where n is schema size time, O(n) space

Start from the nouns, give each an id, then express each relationship as a field whose type is the referenced object (or a list of it), and use non-null on anything a screen cannot render without.

enum TicketStatus {
  OPEN
  PENDING
  CLOSED
}

type Comment {
  id: ID!
  body: String!
  author: Customer!
}

type Customer {
  id: ID!
  email: String!
  tickets: [Ticket!]!
}

type Agent {
  id: ID!
  name: String!
  assignedTickets: [Ticket!]!
}

type Ticket {
  id: ID!
  subject: String!
  status: TicketStatus!
  creator: Customer!
  assignee: Agent
  comments: [Comment!]!
}

Edge Cases:

  • An unassigned ticket needs a nullable assignee; forcing Ticket.assignee: Agent! would make unassigned tickets unrepresentable
  • An empty comments list is [] and satisfies [Comment!]!; null does not

Quiz

1. A client queries product { prcie } and the schema declares priceCents: Int!. What is the outcome?

Question 1 options

2. Why is User.orders: [Order!]! with Order.buyer: User! a legal schema?

Question 2 options

3. What does the double-underscore prefix mean in a GraphQL schema?

Question 3 options

4. Which statement about object-typed fields is correct?

Question 4 options

Flashcards

Question

Object type in SDL

Answer

type Name { field: Type, other(arg: ArgType): Type } - declares named fields, each with a name and a return type, validated on the server.

Question

What validation guarantees vs execution

Answer

Validation proves the document matches the schema (fields, arguments, selections). It cannot prove resolvers return the declared types; those failures appear during execution.

Question

Relationships in the schema

Answer

Object types reference each other as fields (User.orders: [Order!]!, Order.buyer: User!). Cycles are legal; loop protection comes from depth limits.

Question

Reserved name prefix

Answer

Names starting with __ are reserved for introspection (__typename, __schema, __type) and must not be declared in user schemas.

Revision Notes

Key Takeaways

  • 1. A field declaration is a runtime guarantee enforced by validation, not documentation
  • 2. Object types reference each other, and those references are what make the API a graph rather than a set of endpoints
  • 3. Validation runs before execution, so schema mistakes never reach resolvers or databases
  • 4. Field names are unique per type and the __ prefix belongs to introspection

Interview Tips

  • • Answer "what stops a client from querying a nonexistent field?" with the phase, the absence of side effects, and the tool that surfaces the error
  • • Be ready to draw a small domain as object types with references and defend each relationship choice

Cheat Sheet

Object types and fields

type Product {
  id: ID!
  title: String!
  priceCents: Int!
  reviews(first: Int): [Review!]!
}

Rules

  • Every field has a name and a return type; names unique within a type
  • Object field in a query needs a selection set; leaf field must not have one
  • References between types create the graph; cycles are legal (guard with depth limits)
  • __ prefix reserved for introspection
  • Arguments live on fields, not types

Pipeline: parse -> validate (fields, args, selections) -> execute (resolvers, type coercion)

Continue Learning

Next Topics

Related Reading