Skip to main content

What is the Type System?

When you register a service, Contract automatically creates JSON schemas from your Go types. Think of schemas as a universal language that describes the shape of your data - what fields exist, what types they have, and which ones are required. These schemas are used for:
  • API documentation - OpenAPI specs include accurate type information so developers know exactly what to send
  • Input validation - Requests can be validated against the schema before hitting your code
  • Client code generation - Generate typed clients in TypeScript, Python, or any language
  • AI tool definitions - MCP tells AI assistants exactly what parameters your tools accept

How Types are Discovered

When you call contract.Register[todo.API](impl), Contract inspects every method in your interface and extracts the input and output types:
package todo

type API interface {
    // Input: CreateInput, Output: Todo
    Create(ctx context.Context, in *CreateInput) (*Todo, error)

    // Input: none (just context), Output: ListOutput
    List(ctx context.Context) (*ListOutput, error)

    // Input: GetInput, Output: Todo
    Get(ctx context.Context, in *GetInput) (*Todo, error)
}
Contract discovers CreateInput, Todo, ListOutput, and GetInput. It also recursively discovers any types those structs reference. Each unique type is registered once and stored in the service descriptor.

Go to JSON Schema Conversion

Contract converts Go types to JSON Schema types automatically. This table shows the mapping:

Primitive Types

Go TypeJSON Schema TypeJSON Example
string{"type": "string"}"hello"
int, int8, int16, int32, int64{"type": "integer"}42
uint, uint8, uint16, uint32, uint64{"type": "integer"}42
float32, float64{"type": "number"}3.14
bool{"type": "boolean"}true
time.Time{"type": "string", "format": "date-time"}"2024-01-15T10:30:00Z"
[]byte{"type": "string", "format": "byte"}"SGVsbG8=" (base64)

Struct Types

Structs become JSON objects with properties. Each exported field becomes a property:
package user

type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Age       int       `json:"age"`
    IsActive  bool      `json:"isActive"`
    CreatedAt time.Time `json:"createdAt"`
}
This produces the following JSON Schema:
{
    "type": "object",
    "properties": {
        "id": {"type": "string"},
        "name": {"type": "string"},
        "email": {"type": "string"},
        "age": {"type": "integer"},
        "isActive": {"type": "boolean"},
        "createdAt": {"type": "string", "format": "date-time"}
    },
    "required": ["id", "name", "email", "age", "isActive", "createdAt"]
}
Notice that all fields without omitempty are marked as required.

Slice/Array Types

Go slices become JSON arrays:
package todo

type ListOutput struct {
    Items []*Todo `json:"items"`
    Total int     `json:"total"`
}
Produces:
{
    "type": "object",
    "properties": {
        "items": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/Todo"}
        },
        "total": {"type": "integer"}
    },
    "required": ["items", "total"]
}
The $ref syntax means “reference to the Todo schema defined elsewhere.” This avoids duplicating the Todo schema everywhere it’s used.

Map Types

Go maps become JSON objects with dynamic keys:
package config

type Config struct {
    Settings map[string]string `json:"settings"`
    Metadata map[string]any    `json:"metadata"`
}
Produces:
{
    "type": "object",
    "properties": {
        "settings": {
            "type": "object",
            "additionalProperties": {"type": "string"}
        },
        "metadata": {
            "type": "object",
            "additionalProperties": {}
        }
    },
    "required": ["settings", "metadata"]
}
The additionalProperties field describes what type the map values can be.

Nested Structs

Nested structs are automatically discovered and referenced:
package order

type Order struct {
    ID       string   `json:"id"`
    Customer *User    `json:"customer"`
    Items    []*Item  `json:"items"`
    Shipping *Address `json:"shipping"`
}

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type Item struct {
    ProductID string  `json:"productId"`
    Quantity  int     `json:"quantity"`
    Price     float64 `json:"price"`
}

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}
Contract generates schemas for Order, User, Item, and Address, with the Order schema referencing the others:
{
    "type": "object",
    "properties": {
        "id": {"type": "string"},
        "customer": {"$ref": "#/components/schemas/User"},
        "items": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/Item"}
        },
        "shipping": {"$ref": "#/components/schemas/Address"}
    },
    "required": ["id", "customer", "items", "shipping"]
}

Pointer Types

Pointer types are treated the same as non-pointer types in the schema. The pointer affects nullability at runtime but doesn’t change the schema:
package todo

type UpdateInput struct {
    Title *string `json:"title"` // Can be null in JSON
    Count int     `json:"count"` // Cannot be null in JSON
}

JSON Tags

Contract uses the json struct tag to determine field names and behavior. Let’s explore all the options.

Field Naming

The json tag controls the field name in JSON:
package todo

type Todo struct {
    ID        string `json:"id"`         // JSON field: "id"
    Title     string `json:"title"`      // JSON field: "title"
    CreatedAt string `json:"created_at"` // JSON field: "created_at"
    UpdatedAt string `json:"updatedAt"`  // JSON field: "updatedAt"
}
Without tags, Go field names are used as-is (including uppercase first letter):
package todo

type Todo struct {
    ID    string // JSON field: "ID" (not recommended)
    Title string // JSON field: "Title" (not recommended)
}
Best practice: Always use json tags with lowercase or camelCase field names. This matches JSON conventions and makes your API easier to use from JavaScript.

Optional Fields with omitempty

Use omitempty for optional fields. Fields with omitempty:
  • Are not marked as “required” in the schema
  • Are omitted from JSON output when they have their zero value
package todo

type UpdateInput struct {
    ID    string `json:"id"`              // Required in JSON schema
    Title string `json:"title,omitempty"` // Optional in JSON schema
    Phone string `json:"phone,omitempty"` // Optional in JSON schema
}

Excluding Fields

Use - to completely exclude a field from JSON (both serialization and schema):
package user

type User struct {
    ID           string `json:"id"`
    Name         string `json:"name"`
    PasswordHash string `json:"-"` // Never appears in JSON or schema
}
This is essential for sensitive fields that should never leave your server.

Accessing Type Information

After registration, you can access type information from the service descriptor. This is useful for building tools, documentation, or debugging.

Getting All Types

package main

import (
    "fmt"

    contract "github.com/go-mizu/mizu/contract/v2"
    "yourapp/todo"
)

func main() {
    svc := contract.Register[todo.API](impl,
        contract.WithDefaultResource("todos"),
    )

    desc := svc.Descriptor()

    fmt.Println("Registered Types:")
    for _, t := range desc.Types {
        fmt.Printf("  %s (%s)\n", t.Name, t.Kind)
        for _, f := range t.Fields {
            optional := ""
            if f.Optional {
                optional = " (optional)"
            }
            fmt.Printf("    - %s: %s%s\n", f.Name, f.Type, optional)
        }
    }
}
Output:
Registered Types:
  CreateInput (struct)
    - title: string
  GetInput (struct)
    - id: string
  Todo (struct)
    - id: string
    - title: string
    - completed: boolean
  ListOutput (struct)
    - items: []Todo
    - total: integer

Type Definition Structure

Each type in the descriptor has this structure:
type Type struct {
    Name        string    // Type name: "CreateInput", "Todo", etc.
    Description string    // From documentation (if available)
    Kind        TypeKind  // "struct", "slice", "map", "union"
    Fields      []Field   // For struct types: list of fields
    Elem        string    // For slice/map: element type name
}

type Field struct {
    Name        string // Field name from json tag: "title", "id"
    Description string // From `desc` tag (if present)
    Type        string // Type reference: "string", "integer", "Todo"
    Optional    bool   // True if field has omitempty
    Nullable    bool   // True if field is a pointer type
}

Type Kinds

Contract supports several type kinds:

Struct

The most common kind - a collection of named fields:
// Kind: "struct"
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

Slice

An ordered collection of elements:
// Kind: "slice", Elem: "User"
type Users []*User

Map

A dictionary with string keys:
// Kind: "map", Elem: "string"
type Headers map[string]string

Union (Future)

Discriminated unions for “one of these types” scenarios:
// Kind: "union" (not yet implemented)
// Coming in a future version

Additional Field Tags

Beyond json, Contract recognizes additional struct tags for documentation and behavior.

Description Tag

Add descriptions that appear in OpenAPI docs and AI tool definitions:
package todo

type CreateInput struct {
    Title    string `json:"title" desc:"The todo item title (required)"`
    Priority int    `json:"priority" desc:"Priority from 1 (low) to 5 (high), default is 3"`
}
These descriptions help both humans reading the docs and AI assistants understanding what values to provide.

Required Tag

Explicitly mark fields as required (overrides omitempty):
package todo

type CreateInput struct {
    Title string `json:"title" required:"true"`  // Definitely required
    Notes string `json:"notes,omitempty"`        // Optional
}

Path Tag

Mark fields as URL path parameters for REST:
package todo

type GetInput struct {
    // This field is extracted from the URL path, not the request body
    ID string `json:"id" path:"id"`
}
When a client calls GET /todos/abc123, the ID field is populated with "abc123" from the URL.

Complete Example

Here’s a complete example showing type definitions and how to inspect them:
// todo/types.go
package todo

import "time"

// Todo represents a single todo item
type Todo struct {
    ID          string    `json:"id" desc:"Unique identifier"`
    Title       string    `json:"title" desc:"The todo title"`
    Description string    `json:"description,omitempty" desc:"Optional detailed description"`
    Completed   bool      `json:"completed" desc:"Whether the todo is done"`
    Priority    int       `json:"priority" desc:"Priority 1-5, higher is more urgent"`
    Tags        []string  `json:"tags,omitempty" desc:"Optional categorization tags"`
    CreatedAt   time.Time `json:"createdAt" desc:"When the todo was created"`
    UpdatedAt   time.Time `json:"updatedAt" desc:"When the todo was last modified"`
}

// CreateInput contains fields for creating a new todo
type CreateInput struct {
    Title       string   `json:"title" required:"true" desc:"The todo title (required)"`
    Description string   `json:"description,omitempty" desc:"Optional description"`
    Priority    int      `json:"priority,omitempty" desc:"Priority 1-5, defaults to 3"`
    Tags        []string `json:"tags,omitempty" desc:"Optional tags"`
}

// GetInput identifies a todo to retrieve
type GetInput struct {
    ID string `json:"id" path:"id" desc:"The todo's unique identifier"`
}

// ListInput contains filters and pagination for listing todos
type ListInput struct {
    Completed *bool  `json:"completed,omitempty" desc:"Filter by completion status"`
    Tag       string `json:"tag,omitempty" desc:"Filter by tag"`
    Limit     int    `json:"limit,omitempty" desc:"Max items to return (default 20)"`
    Offset    int    `json:"offset,omitempty" desc:"Items to skip for pagination"`
}

// ListOutput contains paginated todo results
type ListOutput struct {
    Items []*Todo `json:"items" desc:"The list of todos"`
    Total int     `json:"total" desc:"Total count (for pagination)"`
}
// todo/api.go
package todo

import "context"

type API interface {
    Create(ctx context.Context, in *CreateInput) (*Todo, error)
    Get(ctx context.Context, in *GetInput) (*Todo, error)
    List(ctx context.Context, in *ListInput) (*ListOutput, error)
}
// main.go - inspecting types
package main

import (
    "fmt"

    contract "github.com/go-mizu/mizu/contract/v2"
    "yourapp/todo"
)

func main() {
    impl := todo.NewService()
    svc := contract.Register[todo.API](impl,
        contract.WithDefaultResource("todos"),
    )

    desc := svc.Descriptor()

    // Print methods and their types
    fmt.Println("=== Methods ===")
    for _, res := range desc.Resources {
        for _, m := range res.Methods {
            fmt.Printf("%s.%s\n", res.Name, m.Name)
            if m.Input != nil {
                fmt.Printf("  Input:  %s\n", m.Input.Name)
            }
            if m.Output != nil {
                fmt.Printf("  Output: %s\n", m.Output.Name)
            }
        }
    }

    // Print all types
    fmt.Println("\n=== Types ===")
    for _, t := range desc.Types {
        fmt.Printf("%s:\n", t.Name)
        for _, f := range t.Fields {
            optional := ""
            if f.Optional {
                optional = " (optional)"
            }
            fmt.Printf("  - %s: %s%s\n", f.Name, f.Type, optional)
            if f.Description != "" {
                fmt.Printf("      %s\n", f.Description)
            }
        }
    }
}

Tips for Good Type Design

Use Clear, Descriptive Names

Names should communicate purpose:
// Good: clear purpose from the name
type CreateUserInput struct { ... }
type UpdateOrderInput struct { ... }
type OrderSummaryOutput struct { ... }

// Bad: vague, could mean anything
type Input struct { ... }
type Data struct { ... }
type Result struct { ... }

Use Appropriate Go Types

Choose the right Go type for each field:
package todo

type Todo struct {
    ID        string    `json:"id"`        // IDs are strings (UUIDs, etc.)
    Title     string    `json:"title"`     // Text content
    Count     int       `json:"count"`     // Whole numbers
    Price     float64   `json:"price"`     // Decimal numbers
    Active    bool      `json:"active"`    // True/false flags
    CreatedAt time.Time `json:"createdAt"` // Timestamps
    Tags      []string  `json:"tags"`      // Lists
}
Organize types by purpose:
// Input types - what clients send
type CreateTodoInput struct { ... }
type UpdateTodoInput struct { ... }
type DeleteTodoInput struct { ... }

// Output types - what you return
type Todo struct { ... }
type TodoList struct { ... }

// Shared types - used in multiple places
type Pagination struct { ... }
type SortOptions struct { ... }

Keep Types Focused

Each type should have one clear purpose:
// Good: each type serves one purpose
type CreateInput struct {
    Title string `json:"title"`  // Only field needed to create
}

type UpdateInput struct {
    ID        string `json:"id"`        // Which todo
    Title     string `json:"title"`     // New title
    Completed bool   `json:"completed"` // New status
}

// Bad: one type for everything (confusing)
type TodoInput struct {
    ID        string `json:"id,omitempty"`
    Title     string `json:"title,omitempty"`
    Completed *bool  `json:"completed,omitempty"`
    // Which operation is this for? Who knows!
}

Common Questions

Are all fields required by default?

Yes. Exported struct fields without omitempty are marked as required in the schema. Add omitempty to make a field optional:
type Input struct {
    Name  string `json:"name"`           // Required
    Email string `json:"email,omitempty"` // Optional
}

Can I use interfaces as field types?

No. Contract needs concrete types to generate schemas. If you have a field that could be multiple types, consider:
  1. Using a struct with optional fields
  2. Creating separate methods for each type
  3. Using map[string]any for truly dynamic data (loses type safety)

How do I handle nullable fields?

Use pointer types for fields that can be explicitly null (as opposed to just missing):
type UpdateInput struct {
    Title *string `json:"title"` // Can be: missing, null, or "value"
    Count int     `json:"count"` // Can be: missing or number (0 is valid)
}
With a pointer:
  • nil means “not provided” or “set to null”
  • "" (empty string) means “set to empty string”
Without a pointer:
  • "" could mean either “not provided” or “set to empty”

What happens to unexported fields?

Unexported fields (lowercase names) are ignored. They don’t appear in JSON or schemas:
type User struct {
    ID       string `json:"id"`   // Included
    Name     string `json:"name"` // Included
    password string               // Ignored (unexported)
}

Can I customize the generated schema?

Not directly. Design your Go types to produce the schema you want. The mapping from Go to JSON Schema is deterministic, so you can predict the output by understanding the rules in this document. If you need very specific schema features not supported by Go types, consider:
  1. Using a code generator to create types from an existing schema
  2. Generating OpenAPI specs and editing them manually
  3. Contributing a feature request to Contract

What’s Next?

Now that you understand the type system: