This guide explains how GraphQL schemas and type systems work, with visual diagrams and practical examples for both developers and newcomers.
┌──────────────────────────────────────────────────────────────┐
│ GraphQL Request Lifecycle │
├──────────────────────────────────────────────────────────────┤
│ 1. Client sends query/mutation │
│ 2. Server parses query and looks up types in the schema │
│ 3. Schema validates all fields, arguments, and types │
│ 4. Execution engine resolves fields using schema info │
│ 5. Response is built and returned to client │
└──────────────────────────────────────────────────────────────┘
┌────────────┐ uses ┌──────────────┐ validates ┌──────────────┐
│ Query AST │ ───────▶ │ Schema │ ────────────▶ │ Type System │
└────────────┘ │ (SDL parsed) │ │ (Rust types) │
└──────────────┘ └──────────────┘
type User {
id: ID!
name: String!
posts: [Post!]!
}
Becomes in Rust:
pub struct ObjectTypeDefinition {
pub name: String, // "User"
pub fields: IndexMap<String, FieldDefinition>,
// ...
}
GraphQL Type System
├── Scalar (Int, String, Boolean, ID, Float)
├── Object (User, Post, ...)
│ └── Fields (id, name, ...)
├── Interface (e.g. Node)
├── Union (e.g. SearchResult = User | Post)
├── Enum (e.g. Status = DRAFT | PUBLISHED)
├── Input Object (e.g. CreateUserInput)
├── List ([Type])
└── Non-Null (Type!)
1. Parse SDL → AST
2. Build Rust type system from AST
3. Validate:
- Query type exists?
- All referenced types defined?
- Field/argument names unique?
- Unions only contain object types?
- Interfaces implemented correctly?
4. If valid, schema is ready for execution!
The GraphQL type system defines the capabilities of a GraphQL API. It describes the complete set of possible data (as a graph of nodes and connections) that a client can access.
// Core type system enums
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TypeDefinition {
Scalar(ScalarTypeDefinition),
Object(ObjectTypeDefinition),
Interface(InterfaceTypeDefinition),
Union(UnionTypeDefinition),
Enum(EnumTypeDefinition),
InputObject(InputObjectTypeDefinition),
}
// Type references with modifiers
#[derive(Debug, Clone, PartialEq)]
pub enum TypeReference {
Named(String),
List(Box<TypeReference>),
NonNull(Box<TypeReference>),
}
// Built-in scalar types
#[derive(Debug, Clone, PartialEq)]
pub enum BuiltinScalar {
Int,
Float,
String,
Boolean,
ID,
}
Object types are the most common types in GraphQL schemas:
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectTypeDefinition {
pub name: String,
pub description: Option<String>,
pub fields: IndexMap<String, FieldDefinition>,
pub interfaces: Vec<String>,
pub directives: Vec<DirectiveApplication>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldDefinition {
pub name: String,
pub description: Option<String>,
pub type_reference: TypeReference,
pub arguments: IndexMap<String, ArgumentDefinition>,
pub directives: Vec<DirectiveApplication>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ArgumentDefinition {
pub name: String,
pub description: Option<String>,
pub type_reference: TypeReference,
pub default_value: Option<Value>,
pub directives: Vec<DirectiveApplication>,
}
Interfaces define a common set of fields that implementing types must include:
#[derive(Debug, Clone, PartialEq)]
pub struct InterfaceTypeDefinition {
pub name: String,
pub description: Option<String>,
pub fields: IndexMap<String, FieldDefinition>,
pub interfaces: Vec<String>, // GraphQL 2018+ supports interface inheritance
pub directives: Vec<DirectiveApplication>,
}
Union types represent objects that could be one of several types:
#[derive(Debug, Clone, PartialEq)]
pub struct UnionTypeDefinition {
pub name: String,
pub description: Option<String>,
pub types: Vec<String>,
pub directives: Vec<DirectiveApplication>,
}
Enum types are scalar types with a finite set of possible values:
#[derive(Debug, Clone, PartialEq)]
pub struct EnumTypeDefinition {
pub name: String,
pub description: Option<String>,
pub values: IndexMap<String, EnumValueDefinition>,
pub directives: Vec<DirectiveApplication>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EnumValueDefinition {
pub name: String,
pub description: Option<String>,
pub deprecation_reason: Option<String>,
pub directives: Vec<DirectiveApplication>,
}
The schema ties everything together:
#[derive(Debug, Clone, PartialEq)]
pub struct SchemaDefinition {
pub description: Option<String>,
pub query: Option<String>,
pub mutation: Option<String>,
pub subscription: Option<String>,
pub directives: Vec<DirectiveApplication>,
}
#[derive(Debug, Clone)]
pub struct Schema {
pub schema_definition: Option<SchemaDefinition>,
pub types: IndexMap<String, TypeDefinition>,
pub directives: IndexMap<String, DirectiveDefinition>,
}
We’ll implement a recursive descent parser that converts GraphQL SDL text into our AST:
pub struct SchemaParser {
lexer: Lexer,
current_token: Token,
peek_token: Token,
}
impl SchemaParser {
pub fn new(input: &str) -> Self {
let mut lexer = Lexer::new(input);
let current_token = lexer.next_token();
let peek_token = lexer.next_token();
Self {
lexer,
current_token,
peek_token,
}
}
pub fn parse_schema_document(&mut self) -> Result<Document, ParseError> {
let mut definitions = Vec::new();
while !self.is_at_end() {
definitions.push(self.parse_definition()?);
}
Ok(Document { definitions })
}
fn parse_definition(&mut self) -> Result<Definition, ParseError> {
match &self.current_token {
Token::Type => self.parse_object_type_definition(),
Token::Interface => self.parse_interface_type_definition(),
Token::Union => self.parse_union_type_definition(),
Token::Enum => self.parse_enum_type_definition(),
Token::Input => self.parse_input_object_type_definition(),
Token::Schema => self.parse_schema_definition(),
Token::Scalar => self.parse_scalar_type_definition(),
Token::Directive => self.parse_directive_definition(),
_ => Err(ParseError::unexpected_token(self.current_token.clone())),
}
}
}
Type references handle the complexity of lists and non-null modifiers:
impl SchemaParser {
fn parse_type_reference(&mut self) -> Result<TypeReference, ParseError> {
let mut type_ref = self.parse_named_type()?;
// Handle list and non-null wrappers
loop {
match &self.current_token {
Token::LeftBracket => {
self.advance(); // consume '['
type_ref = TypeReference::List(Box::new(type_ref));
self.expect_token(Token::RightBracket)?;
}
Token::Bang => {
self.advance(); // consume '!'
type_ref = TypeReference::NonNull(Box::new(type_ref));
}
_ => break,
}
}
Ok(type_ref)
}
fn parse_named_type(&mut self) -> Result<TypeReference, ParseError> {
if let Token::Name(name) = &self.current_token {
let type_name = name.clone();
self.advance();
Ok(TypeReference::Named(type_name))
} else {
Err(ParseError::expected_name())
}
}
}
Our schema validator will enforce GraphQL specification rules:
pub struct SchemaValidator;
impl SchemaValidator {
pub fn validate(&self, schema: &Schema) -> ValidationResult {
let mut errors = Vec::new();
// Rule: Schema must have Query type
self.validate_query_type_exists(schema, &mut errors);
// Rule: All types must be defined
self.validate_type_references(schema, &mut errors);
// Rule: Interface implementations must be valid
self.validate_interface_implementations(schema, &mut errors);
// Rule: Union types must contain object types
self.validate_union_members(schema, &mut errors);
// Rule: Field names must be unique within types
self.validate_field_uniqueness(schema, &mut errors);
// Rule: Argument names must be unique within fields
self.validate_argument_uniqueness(schema, &mut errors);
if errors.is_empty() {
ValidationResult::Valid
} else {
ValidationResult::Invalid(errors)
}
}
fn validate_query_type_exists(&self, schema: &Schema, errors: &mut Vec<ValidationError>) {
let query_type_name = schema
.schema_definition
.as_ref()
.and_then(|def| def.query.as_ref())
.unwrap_or(&"Query".to_string());
if !schema.types.contains_key(query_type_name) {
errors.push(ValidationError::missing_query_type());
}
}
fn validate_type_references(&self, schema: &Schema, errors: &mut Vec<ValidationError>) {
for (type_name, type_def) in &schema.types {
self.validate_type_definition_references(type_name, type_def, schema, errors);
}
}
}
GraphQL servers must provide introspection capabilities:
pub fn build_introspection_schema() -> Schema {
let mut types = IndexMap::new();
// Add introspection types
types.insert("__Schema".to_string(), build_schema_type());
types.insert("__Type".to_string(), build_type_type());
types.insert("__Field".to_string(), build_field_type());
types.insert("__InputValue".to_string(), build_input_value_type());
types.insert("__EnumValue".to_string(), build_enum_value_type());
types.insert("__Directive".to_string(), build_directive_type());
types.insert("__DirectiveLocation".to_string(), build_directive_location_enum());
types.insert("__TypeKind".to_string(), build_type_kind_enum());
Schema {
schema_definition: None,
types,
directives: build_introspection_directives(),
}
}
fn build_schema_type() -> TypeDefinition {
TypeDefinition::Object(ObjectTypeDefinition {
name: "__Schema".to_string(),
description: Some("A GraphQL Schema defines the capabilities of a GraphQL server.".to_string()),
fields: {
let mut fields = IndexMap::new();
fields.insert("types".to_string(), FieldDefinition {
name: "types".to_string(),
description: Some("A list of all types supported by this server.".to_string()),
type_reference: TypeReference::NonNull(Box::new(
TypeReference::List(Box::new(
TypeReference::NonNull(Box::new(TypeReference::Named("__Type".to_string())))
))
)),
arguments: IndexMap::new(),
directives: Vec::new(),
});
// ... more introspection fields
fields
},
interfaces: Vec::new(),
directives: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_object_type() {
let sdl = r#"
type User {
id: ID!
name: String
email: String!
}
"#;
let mut parser = SchemaParser::new(sdl);
let document = parser.parse_schema_document().unwrap();
assert_eq!(document.definitions.len(), 1);
// More detailed assertions...
}
#[test]
fn test_type_reference_with_lists_and_nulls() {
let sdl = "field: [String!]!";
// Test parsing complex type references
}
#[test]
fn test_schema_validation_missing_query_type() {
let schema = Schema {
schema_definition: None,
types: IndexMap::new(),
directives: IndexMap::new(),
};
let validator = SchemaValidator;
let result = validator.validate(&schema);
assert!(matches!(result, ValidationResult::Invalid(_)));
}
}
The type system integrates with our domain model by:
This type system implementation provides the foundation for query execution, validation, and introspection in our GraphQL server.