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 callcontract.Register[todo.API](impl), Contract inspects every method in your interface and extracts the input and output types:
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 Type | JSON Schema Type | JSON 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:omitempty are marked as required.
Slice/Array Types
Go slices become JSON arrays:$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:additionalProperties field describes what type the map values can be.
Nested Structs
Nested structs are automatically discovered and referenced: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 thejson struct tag to determine field names and behavior. Let’s explore all the options.
Field Naming
Thejson tag controls the field name in JSON:
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
Useomitempty 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):
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
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
Beyondjson, Contract recognizes additional struct tags for documentation and behavior.
Description Tag
Add descriptions that appear in OpenAPI docs and AI tool definitions:Required Tag
Explicitly mark fields as required (overrides omitempty):Path Tag
Mark fields as URL path parameters for REST: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:Group Related Types
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 withoutomitempty 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:- Using a struct with optional fields
- Creating separate methods for each type
- Using
map[string]anyfor 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):nilmeans “not provided” or “set to null”""(empty string) means “set to empty string”
""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:- Using a code generator to create types from an existing schema
- Generating OpenAPI specs and editing them manually
- Contributing a feature request to Contract
What’s Next?
Now that you understand the type system:- Registration - How types are discovered during registration
- OpenAPI - How schemas appear in API documentation
- Error Handling - Returning errors from your methods