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:
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

Struct Types

Structs become JSON objects with properties. Each exported field becomes a property:
This produces the following JSON Schema:
Notice that all fields without omitempty are marked as required.

Slice/Array Types

Go slices become JSON arrays:
Produces:
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:
Produces:
The additionalProperties field describes what type the map values can be.

Nested Structs

Nested structs are automatically discovered and referenced:
Contract generates schemas for Order, User, Item, and Address, with the Order schema referencing the others:

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:

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:
Without tags, Go field names are used as-is (including uppercase first letter):
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

Excluding Fields

Use - to completely exclude a field from JSON (both serialization and 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

Output:

Type Definition Structure

Each type in the descriptor has this structure:

Type Kinds

Contract supports several type kinds:

Struct

The most common kind - a collection of named fields:

Slice

An ordered collection of elements:

Map

A dictionary with string keys:

Union (Future)

Discriminated unions for “one of these types” scenarios:

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:
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):

Path Tag

Mark fields as URL path parameters for REST:
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:

Tips for Good Type Design

Use Clear, Descriptive Names

Names should communicate purpose:

Use Appropriate Go Types

Choose the right Go type for each field:
Organize types by purpose:

Keep Types Focused

Each type should have one clear purpose:

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:

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):
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:

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: