Skip to content
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ en:

type:
unknown_type: "Unknown type '%{type}' for attribute '%{attribute}'. Allowed types: %{allowed}"
option_type_mismatch: "Option 'type:' can only be used with 'object' attribute type, got '%{type}' for '%{attribute}'"
invalid_class: "Option 'type:' for attribute '%{attribute}' must be a Class, got %{value}"
mismatch:
integer: "Attribute '%{attribute}' must be an Integer, got %{actual}"
string: "Attribute '%{attribute}' must be a String, got %{actual}"
Expand All @@ -20,6 +22,7 @@ en:
date: "Attribute '%{attribute}' must be a Date, got %{actual}"
time: "Attribute '%{attribute}' must be a Time, got %{actual}"
datetime: "Attribute '%{attribute}' must be a DateTime, got %{actual}"
class: "Attribute '%{attribute}' must be %{type}, got %{actual}"

inclusion:
invalid_schema: "Option 'inclusion' for attribute '%{attribute}' must have a non-empty array of allowed values"
Expand Down
29 changes: 29 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ object :name
**Parameters:**
- `:name` - Symbol representing the object name
- Special object: `:_self` - Merges attributes to parent level
- `type:` (Optional) - Class that value must be an instance of. Without this option, value must be a Hash.

**Examples:**
```ruby
Expand All @@ -618,6 +619,18 @@ end

# Empty object
object :metadata

# Object with custom type validation
object :author, type: User do
string :name
string :email
end

# Object with type: and use_entity (type-safe entity reuse)
object :author, type: Author do
use_entity(Shared::AuthorEntity)
end
# Value must be Author instance, attributes copied from entity
```

## Attribute Types
Expand Down Expand Up @@ -831,6 +844,22 @@ array :posts do
end
end

# Array of typed instances
array :users do
object :_self, type: User do
string :name
string :email
end
end

# Array with type: and use_entity (type-safe entity reuse)
array :authors do
object :_self, type: Author do
use_entity(Shared::AuthorEntity)
end
end
# Each item must be Author instance, attributes copied from entity

# Empty array
array :items, :optional
```
Expand Down
47 changes: 46 additions & 1 deletion docs/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ object :author do
end
```

**Type validation:** Value must be a Ruby `Hash`
**Type validation:** Value must be a Ruby `Hash`, or an instance of a custom class if specified via `type:` option

**See:** [Nested Structures](./nested-structures.md) for detailed information

Expand Down Expand Up @@ -234,6 +234,51 @@ integer :rating, in: [1, 2, 3, 4, 5]

**Validation:** Value must be one of the specified values.

#### type

Specifies a custom class that object values must be instances of. **Only works with `object` attribute type.**

```ruby
# Simple mode
object :author, type: User do
string :name
string :email
end

# Advanced mode with custom message
object :author, type: { is: User, message: "Expected a User instance" } do
string :name
string :email
end

# With :_self in arrays (array of typed instances)
array :users do
object :_self, type: User do
string :name
string :email
end
end
```

**Validation:**
- Without `type:` — value must be a `Hash`
- With `type: SomeClass` — value must be an instance of `SomeClass`

**Note:** When `type:` is specified, ONLY instances of that class are accepted. Hash is NOT accepted.

**Important:** Can only be used with `object` attribute type. Using `type:` on strings, integers, arrays, etc. raises a schema validation error.

**With `use_entity` (typed object with entity reuse):**
```ruby
# Combine type: with use_entity for type-safe entity reuse
object :author, type: Author do
use_entity(Shared::AuthorEntity)
end
# Value must be Author instance, attributes copied from entity
```

**See:** [Validation: Type Validation](./validation.md#type-validation) for detailed examples

#### format

Validates that string values match specific formats. **Only works with string type attributes.**
Expand Down
33 changes: 33 additions & 0 deletions docs/cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ array :socials, :optional do
end
```

### Combine use_entity with type:

```ruby
# Type-safe entity reuse (only Author instances accepted)
object :author, type: Author do
use_entity(Shared::AuthorEntity)
end
# Data: Author.new(name: "John", email: "john@example.com")

# In arrays
array :authors do
object :_self, type: Author do
use_entity(Shared::AuthorEntity)
end
end
# Data: [Author.new(name: "John"), Author.new(name: "Jane")]
```

**Note:** When `type:` is specified, Hash is NOT accepted.

### Organize Entities

```
Expand Down Expand Up @@ -309,6 +329,11 @@ object :settings, :optional do
string :theme
end

# Object with custom type
object :author, type: User do
string :name
end

# Deeply nested
object :post do
object :author do
Expand Down Expand Up @@ -361,6 +386,14 @@ end
array :tags, :optional do
string :_self
end

# Array of typed instances
array :users do
object :_self, type: User do
string :name
end
end
# Data: [User.new(name: "John"), User.new(name: "Jane")]
```

## Request Definition
Expand Down
67 changes: 67 additions & 0 deletions docs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,59 @@ Use `use_entity` in nested blocks when:
- You want to maintain a single source of truth for the structure
- The nested structure is complex enough to warrant its own class

### Entity Reuse with Custom Types

Combine `use_entity` with `type:` option for type-safe entity reuse:

```ruby
module Posts
module Create
class ResponseEntity < ApplicationEntity
object :post do
string :id
string :title

# Reuse entity with type validation
object :author, type: Author do
use_entity(Shared::AuthorEntity)
end

# In arrays
array :collaborators, :optional do
object :_self, type: Collaborator do
use_entity(Shared::CollaboratorEntity)
end
end
end
end
end
end
```

**How it works:**
- `type: Author` — validates that value is an `Author` instance
- `use_entity(Entity)` — copies attribute definitions for nested validation
- Values extracted via `author.name` (not `hash[:name]`)

**Note:** When `type:` is specified, the value must be an instance of that class.
Hashes are NOT accepted. Use `type:` only when your service returns typed objects.

**Expected data:**
```ruby
# Only Author/Collaborator instances accepted
{
post: {
id: "123",
title: "Hello",
author: Author.new(name: "John", bio: "Developer"),
collaborators: [
Collaborator.new(name: "Jane", role: "Editor"),
Collaborator.new(name: "Bob", role: "Reviewer")
]
}
}
```

## Internal Architecture

Understanding how Entity classes work internally:
Expand Down Expand Up @@ -455,6 +508,20 @@ class ProductEntity < Treaty::Entity::Base
# Validation
string :category, in: %w[electronics clothing food]

# Custom type validation (only for objects)
object :author, type: Author do
string :name
string :email
end

# Array of typed instances
array :collaborators, :optional do
object :_self, type: Collaborator do
string :name
string :role
end
end

# Computed values (derive from other attributes)
string :display_name, :optional, computed: (lambda do |**attributes|
"#{attributes.dig(:name)} (#{attributes.dig(:sku) || 'N/A'})"
Expand Down
9 changes: 8 additions & 1 deletion docs/internationalization.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ treaty:

type:
unknown_type: "Unknown type '%{type}' for attribute '%{attribute}'. Allowed types: %{allowed}"
option_type_mismatch: "Option 'type:' can only be used with 'object' attribute type, got '%{type}' for '%{attribute}'"
invalid_class: "Option 'type:' for attribute '%{attribute}' must be a Class, got %{value}"
mismatch:
integer: "Attribute '%{attribute}' must be an Integer, got %{actual}"
string: "Attribute '%{attribute}' must be a String, got %{actual}"
Expand All @@ -79,6 +81,7 @@ treaty:
date: "Attribute '%{attribute}' must be a Date, got %{actual}"
time: "Attribute '%{attribute}' must be a Time, got %{actual}"
datetime: "Attribute '%{attribute}' must be a DateTime, got %{actual}"
class: "Attribute '%{attribute}' must be %{type}, got %{actual}"

inclusion:
invalid_schema: "Option 'inclusion' for attribute '%{attribute}' must have a non-empty array of allowed values"
Expand Down Expand Up @@ -213,6 +216,8 @@ de:

type:
unknown_type: "Unbekannter Typ '%{type}' für Attribut '%{attribute}'. Erlaubte Typen: %{allowed}"
option_type_mismatch: "Option 'type:' kann nur mit 'object' Attributtyp verwendet werden, '%{type}' für '%{attribute}' erhalten"
invalid_class: "Option 'type:' für Attribut '%{attribute}' muss eine Klasse sein, %{value} erhalten"
mismatch:
integer: "Attribut '%{attribute}' muss ein Integer sein, aber %{actual} erhalten"
string: "Attribut '%{attribute}' muss ein String sein, aber %{actual} erhalten"
Expand All @@ -222,6 +227,7 @@ de:
date: "Attribut '%{attribute}' muss ein Date sein, aber %{actual} erhalten"
time: "Attribut '%{attribute}' muss ein Time sein, aber %{actual} erhalten"
datetime: "Attribut '%{attribute}' muss ein DateTime sein, aber %{actual} erhalten"
class: "Attribut '%{attribute}' muss %{type} sein, aber %{actual} erhalten"

inclusion:
invalid_schema: "Option 'inclusion' für Attribut '%{attribute}' muss ein nicht-leeres Array von erlaubten Werten haben"
Expand Down Expand Up @@ -473,8 +479,9 @@ Different validators provide different interpolation variables:
**Type validator:**
- `%{attribute}` - the attribute name
- `%{actual}` - the actual type received
- `%{type}` - the expected type
- `%{type}` - the expected type (or attribute type for option_type_mismatch)
- `%{allowed}` - list of allowed types (for unknown_type)
- `%{value}` - the invalid value provided (for invalid_class)

**Inclusion validator:**
- `%{attribute}` - the attribute name
Expand Down
Loading