> For the complete documentation index, see [llms.txt](https://light-services.kodkod.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://light-services.kodkod.me/deep-dive/sorbet-runtime.md).

# Sorbet Runtime Types

Operandi supports [Sorbet runtime types](https://sorbet.org/docs/runtime) for type validation of arguments and outputs. This provides runtime type checking using Sorbet's type system.

{% hint style="info" %}
This page covers **runtime type checking** with `sorbet-runtime`. For **static type analysis** with Sorbet and RBI file generation, see [Tapioca / Sorbet Integration](/deep-dive/tapioca.md).
{% endhint %}

## Installation

Add `sorbet-runtime` to your Gemfile:

```ruby
gem "sorbet-runtime"
```

Then run:

```bash
bundle install
```

## Basic Usage

When `sorbet-runtime` is loaded, plain Ruby classes are automatically validated using Sorbet's type system:

```ruby
require "sorbet-runtime"

class User::Create < ApplicationService
  # Basic types - plain Ruby classes work directly!
  arg :name, type: String
  arg :age, type: Integer
  
  # Nilable types (allows nil)
  arg :email, type: T.nilable(String), optional: true
  
  # Union types (multiple allowed types)
  arg :status, type: T.any(String, Symbol), default: "pending"
  
  # Typed arrays
  arg :tags, type: T::Array[String], optional: true
  
  # Boolean type
  arg :active, type: T::Boolean, default: true

  # Outputs with Sorbet types - plain classes work here too
  output :user, type: User
  output :metadata, type: Hash
  
  step :create_user
  step :build_metadata

  private

  def create_user
    self.user = User.create!(
      name: name,
      age: age,
      email: email,
      status: status,
      tags: tags || [],
      active: active
    )
  end

  def build_metadata
    self.metadata = { created_at: Time.current }
  end
end
```

## Type Reference

### Basic Types

When `sorbet-runtime` is loaded, plain Ruby classes are automatically coerced to Sorbet types:

```ruby
arg :name, type: String
arg :count, type: Integer
arg :price, type: Float
arg :data, type: Hash
arg :items, type: Array
```

{% hint style="info" %}
You can also use `T::Utils.coerce(String)` explicitly, but it's not required - Operandi handles the coercion automatically.
{% endhint %}

### Nilable Types

Allow `nil` values with `T.nilable`:

```ruby
arg :nickname, type: T.nilable(String), optional: true
```

### Union Types

Allow multiple types with `T.any`:

```ruby
arg :identifier, type: T.any(String, Integer)
arg :status, type: T.any(String, Symbol)
```

### Typed Arrays

Validate array element types with `T::Array`:

```ruby
arg :tags, type: T::Array[String]
arg :numbers, type: T::Array[Integer]
arg :users, type: T::Array[User]
```

{% hint style="info" %}
**Generic Type Erasure:** Sorbet's `T::Array[String]` only validates that the value is an `Array` at runtime. The type parameter (`String`) is **erased** and not checked at runtime. For strict element validation, implement custom validation in your service steps.
{% endhint %}

### Boolean Type

Use `T::Boolean` for true/false values:

```ruby
arg :active, type: T::Boolean
arg :verified, type: T::Boolean, default: false
```

### Complex Types

Combine types for more complex validations:

```ruby
# Nilable array
arg :tags, type: T.nilable(T::Array[String]), optional: true

# Array of union types
arg :identifiers, type: T::Array[T.any(String, Integer)]
```

## Important: Sorbet Types Validate Only

{% hint style="warning" %}
**Sorbet types do NOT coerce values.** Sorbet runtime types only validate that values match the expected type. They will not automatically convert values (e.g., `"123"` will not become `123`).
{% endhint %}

### Validation Behavior

```ruby
# Sorbet runtime type
arg :age, type: Integer

# Valid - passes validation
service = MyService.run(age: 25)
service.age # => 25 (Integer)

# Invalid - raises error
service = MyService.run(age: "25")
# => Raises ArgTypeError: expected Integer, got String
```

If you need coercion, handle it explicitly in your service:

```ruby
class MyService < ApplicationService
  arg :age, type: Integer

  step :coerce_inputs
  step :process

  private

  def coerce_inputs
    self.age = age.to_i if age.is_a?(String)
  end
end
```

## Combining with Tapioca

For full Sorbet support, you can use both:

1. **Runtime types** (`sorbet-runtime`) - Validates types at runtime
2. **Static types** (Tapioca) - Generates RBI files for static analysis

See [Tapioca / Sorbet Integration](/deep-dive/tapioca.md) for setting up static type analysis.

```ruby
# typed: strict

class User::Create < ApplicationService
  # Runtime validation with Sorbet types
  arg :name, type: String
  arg :age, type: Integer
  
  output :user, type: User
  
  # ...
end
```

With Tapioca configured, you get:

* **Runtime validation** from `sorbet-runtime`
* **IDE autocompletion** from generated RBI files
* **Static type checking** from `srb tc`

## Error Messages

When type validation fails, Operandi raises `ArgTypeError` with a descriptive message:

```ruby
service = User::Create.run(name: 123, age: 25)
# => Operandi::ArgTypeError: User::Create argument `name` expected String, but got Integer with value: 123
```

## Full Example

```ruby
require "sorbet-runtime"

class Order::Create < ApplicationService
  # Required arguments - plain classes work!
  arg :customer, type: Customer
  arg :items, type: T::Array[OrderItem]
  arg :total, type: T.any(Integer, Float)
  
  # Optional arguments
  arg :notes, type: T.nilable(String), optional: true
  arg :priority, type: T::Boolean, default: false
  arg :tags, type: T::Array[String], optional: true
  
  # Outputs
  output :order, type: Order
  output :confirmation_number, type: String
  
  step :validate_items
  step :create_order
  step :generate_confirmation

  private

  def validate_items
    fail!("Order must have at least one item") if items.empty?
  end

  def create_order
    self.order = Order.create!(
      customer: customer,
      items: items,
      total: total,
      notes: notes,
      priority: priority,
      tags: tags || []
    )
  end

  def generate_confirmation
    self.confirmation_number = "ORD-#{order.id}-#{SecureRandom.hex(4).upcase}"
  end
end

# Usage
result = Order::Create.run(
  customer: current_user,
  items: [item1, item2],
  total: 99.99,
  priority: true
)

if result.success?
  puts "Order created: #{result.confirmation_number}"
else
  puts "Failed: #{result.errors.full_messages}"
end
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://light-services.kodkod.me/deep-dive/sorbet-runtime.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
