easy_talk 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -43
  3. data/CHANGELOG.md +105 -0
  4. data/README.md +510 -2018
  5. data/docs/json_schema_compliance.md +140 -26
  6. data/docs/primitive-schema-rfc.md +894 -0
  7. data/examples/ruby_llm/Gemfile +12 -0
  8. data/examples/ruby_llm/structured_output.rb +47 -0
  9. data/examples/ruby_llm/tools_integration.rb +49 -0
  10. data/lib/easy_talk/builders/base_builder.rb +2 -1
  11. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  12. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  13. data/lib/easy_talk/builders/composition_builder.rb +7 -2
  14. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  15. data/lib/easy_talk/builders/null_builder.rb +4 -1
  16. data/lib/easy_talk/builders/number_builder.rb +4 -1
  17. data/lib/easy_talk/builders/object_builder.rb +64 -3
  18. data/lib/easy_talk/builders/registry.rb +15 -1
  19. data/lib/easy_talk/builders/string_builder.rb +3 -1
  20. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  21. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  22. data/lib/easy_talk/builders/typed_array_builder.rb +4 -2
  23. data/lib/easy_talk/builders/union_builder.rb +5 -1
  24. data/lib/easy_talk/configuration.rb +17 -2
  25. data/lib/easy_talk/errors.rb +1 -0
  26. data/lib/easy_talk/errors_helper.rb +3 -0
  27. data/lib/easy_talk/extensions/ruby_llm_compatibility.rb +58 -0
  28. data/lib/easy_talk/json_schema_equality.rb +46 -0
  29. data/lib/easy_talk/keywords.rb +0 -1
  30. data/lib/easy_talk/model.rb +42 -1
  31. data/lib/easy_talk/model_helper.rb +4 -0
  32. data/lib/easy_talk/naming_strategies.rb +4 -0
  33. data/lib/easy_talk/property.rb +7 -0
  34. data/lib/easy_talk/ref_helper.rb +6 -0
  35. data/lib/easy_talk/schema.rb +1 -0
  36. data/lib/easy_talk/schema_definition.rb +52 -6
  37. data/lib/easy_talk/schema_methods.rb +36 -5
  38. data/lib/easy_talk/sorbet_extension.rb +1 -0
  39. data/lib/easy_talk/type_introspection.rb +45 -1
  40. data/lib/easy_talk/types/tuple.rb +77 -0
  41. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +350 -62
  42. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  43. data/lib/easy_talk/validation_adapters/base.rb +12 -0
  44. data/lib/easy_talk/validation_adapters/none_adapter.rb +9 -0
  45. data/lib/easy_talk/validation_builder.rb +1 -0
  46. data/lib/easy_talk/version.rb +1 -1
  47. data/lib/easy_talk.rb +1 -0
  48. metadata +17 -4
data/README.md CHANGED
@@ -2,591 +2,375 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/easy_talk.svg)](https://badge.fury.io/rb/easy_talk)
4
4
  [![Ruby](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml/badge.svg)](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml)
5
+ [![codecov](https://codecov.io/gh/sergiobayona/easy_talk/graph/badge.svg)](https://codecov.io/gh/sergiobayona/easy_talk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Ruby](https://img.shields.io/badge/ruby-3.2%2B-ruby.svg)](https://www.ruby-lang.org)
8
+ [![Downloads](https://img.shields.io/gem/dt/easy_talk.svg)](https://rubygems.org/gems/easy_talk)
5
9
  [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/easy_talk)
10
+ [![GitHub stars](https://img.shields.io/github/stars/sergiobayona/easy_talk?style=social)](https://github.com/sergiobayona/easy_talk)
6
11
 
7
- ## Table of Contents
12
+ Ruby library for defining **structured data contracts** that generate **JSON Schema** *and* (optionally) **runtime validations** from the same definition.
8
13
 
9
- - [Introduction](#introduction)
10
- - [What is EasyTalk?](#what-is-easytalk)
11
- - [Key Features](#key-features)
12
- - [Use Cases](#use-cases)
13
- - [Inspiration](#inspiration)
14
- - [Installation](#installation)
15
- - [Requirements](#requirements)
16
- - [Version 2.0.0 Breaking Changes](#version-200-breaking-changes)
17
- - [Installation Steps](#installation-steps)
18
- - [Verification](#verification)
19
- - [Quick Start](#quick-start)
20
- - [Minimal Example](#minimal-example)
21
- - [Generated JSON Schema](#generated-json-schema)
22
- - [Basic Usage](#basic-usage)
23
- - [Core Concepts](#core-concepts)
24
- - [Schema Definition](#schema-definition)
25
- - [Property Types](#property-types)
26
- - [Ruby Types](#ruby-types)
27
- - [Sorbet-Style Types](#sorbet-style-types)
28
- - [Custom Types](#custom-types)
29
- - [Property Constraints](#property-constraints)
30
- - [Required vs Optional Properties](#required-vs-optional-properties)
31
- - [Automatic Validation Generation](#automatic-validation-generation)
32
- - [Manual Validation Overrides](#manual-validation-overrides)
33
- - [Defining Schemas](#defining-schemas)
34
- - [Basic Schema Structure](#basic-schema-structure)
35
- - [Property Definitions](#property-definitions)
36
- - [Arrays and Collections](#arrays-and-collections)
37
- - [Constraints and Automatic Validations](#constraints-and-automatic-validations)
38
- - [Supported Constraint-to-Validation Mappings](#supported-constraint-to-validation-mappings)
39
- - [Additional Properties](#additional-properties)
40
- - [Property Naming](#property-naming)
41
- - [Schema Composition](#schema-composition)
42
- - [Using T::AnyOf](#using-tanyof)
43
- - [Using T::OneOf](#using-toneof)
44
- - [Using T::AllOf](#using-tallof)
45
- - [Array Composition](#array-composition)
46
- - [Complex Compositions](#complex-compositions)
47
- - [Reusing Models](#reusing-models)
48
- - [ActiveModel Integration](#activemodel-integration)
49
- - [Enhanced Validation System](#enhanced-validation-system)
50
- - [Error Handling](#error-handling)
51
- - [Standardized Error Formatting](#standardized-error-formatting)
52
- - [Available Formats](#available-formats)
53
- - [Instance Methods](#instance-methods)
54
- - [Direct API Usage](#direct-api-usage)
55
- - [Configuration](#configuration)
56
- - [Model Attributes](#model-attributes)
57
- - [Advanced Features](#advanced-features)
58
- - [Schema-Only Mode (EasyTalk::Schema)](#schema-only-mode-easytalkschema)
59
- - [Key Differences from EasyTalk::Model](#key-differences-from-easytalkmodel)
60
- - [When to Use Each](#when-to-use-each)
61
- - [LLM Function Generation](#llm-function-generation)
62
- - [Schema Transformation](#schema-transformation)
63
- - [Type Checking and Validation](#type-checking-and-validation)
64
- - [Custom Type Builders](#custom-type-builders)
65
- - [Registering Custom Types](#registering-custom-types)
66
- - [Creating a Custom Builder](#creating-a-custom-builder)
67
- - [Collection Type Builders](#collection-type-builders)
68
- - [Overriding Built-in Types](#overriding-built-in-types)
69
- - [Registry API](#registry-api)
70
- - [Type Introspection](#type-introspection)
71
- - [Configuration](#configuration-1)
72
- - [Global Settings](#global-settings)
73
- - [Automatic Validation Configuration](#automatic-validation-configuration)
74
- - [Per-Model Configuration](#per-model-configuration)
75
- - [Validation Adapters](#validation-adapters)
76
- - [Built-in Adapters](#built-in-adapters)
77
- - [Global Adapter Configuration](#global-adapter-configuration)
78
- - [Per-Model Validation Control](#per-model-validation-control)
79
- - [Per-Property Validation Control](#per-property-validation-control)
80
- - [Custom Validation Adapters](#custom-validation-adapters)
81
- - [Examples](#examples)
82
- - [User Registration (with Auto-Validations)](#user-registration-with-auto-validations)
83
- - [Payment Processing](#payment-processing)
84
- - [Complex Object Hierarchies](#complex-object-hierarchies)
85
- - [API Integration](#api-integration)
86
- - [Troubleshooting](#troubleshooting)
87
- - [Common Errors](#common-errors)
88
- - ["Invalid property name"](#invalid-property-name)
89
- - ["Property type is missing"](#property-type-is-missing)
90
- - ["Unknown option"](#unknown-option)
91
- - [Schema Validation Issues](#schema-validation-issues)
92
- - [Type Errors](#type-errors)
93
- - [Best Practices](#best-practices)
94
- - [Nullable vs Optional Properties in EasyTalk](#nullable-vs-optional-properties-in-easytalk)
95
- - [Key Concepts](#key-concepts)
96
- - [Nullable Properties](#nullable-properties)
97
- - [Optional Properties](#optional-properties)
98
- - [Nullable AND Optional Properties](#nullable-and-optional-properties)
99
- - [Configuration Options](#configuration-options)
100
- - [Practical Examples](#practical-examples)
101
- - [User Profile Schema](#user-profile-schema)
102
- - [Common Gotchas](#common-gotchas)
103
- - [Misconception: Nullable Implies Optional](#misconception-nullable-implies-optional)
104
- - [Misconception: Optional Properties Accept Null](#misconception-optional-properties-accept-null)
105
- - [Migration from Earlier Versions](#migration-from-earlier-versions)
106
- - [Best Practices](#best-practices-1)
107
- - [JSON Schema Comparison](#json-schema-comparison)
108
- - [Migration Guide from v1.x to v2.0](#migration-guide-from-v1x-to-v20)
109
- - [Breaking Changes Summary](#breaking-changes-summary)
110
- - [Migration Steps](#migration-steps)
111
- - [1. Replace Hash-based Nested Schemas](#1-replace-hash-based-nested-schemas)
112
- - [2. Review Automatic Validations](#2-review-automatic-validations)
113
- - [3. Configuration Updates](#3-configuration-updates)
114
- - [Compatibility Notes](#compatibility-notes)
115
- - [Development and Contributing](#development-and-contributing)
116
- - [Setting Up the Development Environment](#setting-up-the-development-environment)
117
- - [Running Tests](#running-tests)
118
- - [Code Quality](#code-quality)
119
- - [Contributing Guidelines](#contributing-guidelines)
120
- - [JSON Schema Version (`$schema` Keyword)](#json-schema-version-schema-keyword)
121
- - [Why Use `$schema`?](#why-use-schema)
122
- - [Supported Draft Versions](#supported-draft-versions)
123
- - [Global Configuration](#global-configuration)
124
- - [Per-Model Configuration](#per-model-configuration-1)
125
- - [Disabling `$schema` for Specific Models](#disabling-schema-for-specific-models)
126
- - [Custom Schema URIs](#custom-schema-uris)
127
- - [Nested Models](#nested-models)
128
- - [Default Behavior](#default-behavior)
129
- - [Best Practices](#best-practices-2)
130
- - [Schema Identifier (`$id` Keyword)](#schema-identifier-id-keyword)
131
- - [Why Use `$id`?](#why-use-id)
132
- - [Global Configuration](#global-configuration-1)
133
- - [Per-Model Configuration](#per-model-configuration-2)
134
- - [Disabling `$id` for Specific Models](#disabling-id-for-specific-models)
135
- - [Combining `$schema` and `$id`](#combining-schema-and-id)
136
- - [Nested Models](#nested-models-1)
137
- - [URI Formats](#uri-formats)
138
- - [Default Behavior](#default-behavior-1)
139
- - [Best Practices](#best-practices-3)
140
- - [Schema References (`$ref` and `$defs`)](#schema-references-ref-and-defs)
141
- - [Why Use `$ref`?](#why-use-ref)
142
- - [Default Behavior (Inline Schemas)](#default-behavior-inline-schemas)
143
- - [Enabling `$ref` References](#enabling-ref-references)
144
- - [Global Configuration](#global-configuration-2)
145
- - [Per-Property Configuration](#per-property-configuration)
146
- - [Arrays of Models](#arrays-of-models)
147
- - [Nilable Models with `$ref`](#nilable-models-with-ref)
148
- - [Multiple References to the Same Model](#multiple-references-to-the-same-model)
149
- - [Combining `$ref` with Other Constraints](#combining-ref-with-other-constraints)
150
- - [Interaction with `compose`](#interaction-with-compose)
151
- - [Best Practices](#best-practices-4)
152
- - [Default Behavior](#default-behavior-2)
153
- - [JSON Schema Compatibility](#json-schema-compatibility)
154
- - [Supported Versions](#supported-versions)
155
- - [Specification Compliance](#specification-compliance)
156
- - [Known Limitations](#known-limitations)
157
- - [API Reference](#api-reference)
158
- - [Core Modules](#core-modules)
159
- - [Builders](#builders)
160
- - [Configuration & Utilities](#configuration--utilities)
161
- - [License](#license)
14
+ Think “Pydantic-style ergonomics” for Ruby, with first-class JSON Schema output.
162
15
 
163
- ## Introduction
16
+ ---
164
17
 
165
- ### What is EasyTalk?
166
- EasyTalk is a Ruby library for defining structured data models with automatic JSON Schema generation and flexible validation. Define your schema once and get both a JSON Schema document and runtime validations from a single source of truth.
18
+ ## Why EasyTalk?
167
19
 
168
- ### Key Features
169
- * **Intuitive Schema Definition**: Define JSON Schema using Ruby classes with a clean, declarative DSL.
170
- * **Rich Type System**: Supports Ruby primitives plus Sorbet-style types (`T::Array[Type]`, `T.nilable(Type)`, `T::Boolean`) and composition types (`T::AnyOf`, `T::OneOf`, `T::AllOf`).
171
- * **Automatic Validations**: Schema constraints automatically generate ActiveModel validations, including nested model validation within arrays.
172
- * **API Error Formatting**: Format validation errors in multiple standards (flat, JSON Pointer, RFC 7807, JSON:API).
173
- * **LLM Function Support**: Generate JSON Schema for LLM function calling (OpenAI, Anthropic, etc.).
174
- * **Schema Composition**: Reference models within other models and use `$ref`/`$defs` for reusable definitions.
175
- * **Nested Model Instantiation**: Hash attributes automatically instantiate nested EasyTalk models, including within arrays.
176
- * **Flexible Configuration**: Global and per-model settings for validation behavior, naming strategies, and schema output.
177
- * **JSON Schema Versions**: Support for Draft-04 through Draft 2020-12 with configurable `$schema` and `$id` keywords.
20
+ You can hand-write JSON Schema, then hand-write validations, then hand-write error responses… and eventually you’ll ship a bug where those three disagree.
178
21
 
179
- ### Use Cases
180
- - API request/response validation
181
- - LLM function definitions
182
- - Object structure documentation
183
- - Data validation and transformation
184
- - Configuration schema definitions
22
+ EasyTalk makes the schema definition the single source of truth, so you can:
185
23
 
186
- ### Inspiration
187
- Inspired by Python's [Pydantic](https://docs.pydantic.dev/) library, EasyTalk brings similar functionality to the Ruby ecosystem, providing a Ruby-friendly approach to JSON Schema operations.
24
+ - **Define once, use everywhere**
25
+ One Ruby DSL gives you:
26
+ - `json_schema` for docs, OpenAPI, LLM tools, and external validators
27
+ - `valid?` / `errors` (when using `EasyTalk::Model`) for runtime validation
188
28
 
189
- ## Installation
29
+ - **Stop arguing with JSON Schema’s verbosity**
30
+ Express constraints in Ruby where you already live:
31
+ ```ruby
32
+ property :email, String, format: "email"
33
+ property :age, Integer, minimum: 18
34
+ property :tags, T::Array[String], min_items: 1
35
+ ```
190
36
 
191
- ### Requirements
192
- - Ruby 3.2 or higher
37
+ - **Use a richer type system than "string/integer/object"**
38
+ EasyTalk supports Sorbet-style types and composition:
39
+ - `T.nilable(Type)` for nullable fields
40
+ - `T::Array[Type]` for typed arrays
41
+ - `T::Tuple[Type1, Type2, ...]` for fixed-position typed arrays
42
+ - `T::Boolean`
43
+ - `T::AnyOf`, `T::OneOf`, `T::AllOf` for schema composition
193
44
 
194
- ### Version 2.0.0 Breaking Changes
45
+ - **Get validations for free (when you want them)**
46
+ With `auto_validations` enabled (default), schema constraints generate ActiveModel validations—**including nested models**, even inside arrays.
195
47
 
196
- ⚠️ **IMPORTANT**: Version 2.0.0 includes breaking changes. Please review before upgrading:
48
+ - **Make API errors consistent**
49
+ Format validation errors as:
50
+ - flat lists
51
+ - JSON Pointer
52
+ - **RFC 7807** problem details
53
+ - **JSON:API** error objects
197
54
 
198
- - **Removed**: Block-style nested object definitions (using `Hash do ... end`)
199
- - **Migration**: Use class references instead of inline Hash definitions
55
+ - **LLM tool/function schemas without a second schema layer**
56
+ Use the same contract to generate JSON Schema for function/tool calling. See [RubyLLM Integration](#rubyllm-integration).
200
57
 
201
- ```ruby
202
- # ❌ No longer supported (v1.x style)
203
- define_schema do
204
- property :address, Hash do
205
- property :street, String
206
- property :city, String
207
- end
208
- end
58
+ EasyTalk is for teams who want their data contracts to be **correct, reusable, and boring** (the good kind of boring).
209
59
 
210
- # ✅ New approach (v2.x style)
211
- class Address
212
- include EasyTalk::Model
213
- define_schema do
214
- property :street, String
215
- property :city, String
216
- end
217
- end
60
+ ---
218
61
 
219
- class User
220
- include EasyTalk::Model
221
- define_schema do
222
- property :address, Address # Reference the class directly
223
- end
224
- end
225
- ```
62
+ ## Table of Contents
226
63
 
227
- ### Installation Steps
228
- Add EasyTalk to your application's Gemfile:
64
+ - [Installation](#installation)
65
+ - [Quick start](#quick-start)
66
+ - [Property constraints](#property-constraints)
67
+ - [Core concepts](#core-concepts)
68
+ - [Required vs optional vs nullable](#required-vs-optional-vs-nullable-dont-get-tricked)
69
+ - [Nested models](#nested-models-and-automatic-instantiation)
70
+ - [Tuple arrays](#tuple-arrays-fixed-position-types)
71
+ - [Composition (AnyOf / OneOf / AllOf)](#composition-anyof--oneof--allof)
72
+ - [Validations](#validations)
73
+ - [Automatic validations](#automatic-validations-default)
74
+ - [Per-model validation control](#per-model-validation-control)
75
+ - [Per-property validation control](#per-property-validation-control)
76
+ - [Validation adapters](#validation-adapters)
77
+ - [Error formatting](#error-formatting)
78
+ - [Schema-only mode](#schema-only-mode)
79
+ - [RubyLLM Integration](#rubyllm-integration)
80
+ - [Configuration highlights](#configuration-highlights)
81
+ - [Advanced topics](#advanced-topics)
82
+ - [JSON Schema drafts, `$id`, and `$ref`](#json-schema-drafts-id-and-ref)
83
+ - [Additional properties with types](#additional-properties-with-types)
84
+ - [Object-level constraints](#object-level-constraints)
85
+ - [Custom type builders](#custom-type-builders)
86
+ - [Known limitations](#known-limitations)
87
+ - [Contributing](#contributing)
88
+ - [License](#license)
229
89
 
230
- ```ruby
231
- gem 'easy_talk'
232
- ```
90
+ ---
233
91
 
234
- Or install it directly:
92
+ ## Installation
235
93
 
236
- ```bash
237
- $ gem install easy_talk
238
- ```
94
+ ### Requirements
95
+ - Ruby **3.2+**
239
96
 
240
- ### Verification
241
- After installation, you can verify it's working by creating a simple model:
97
+ Add to your Gemfile:
242
98
 
243
99
  ```ruby
244
- require 'easy_talk'
100
+ gem "easy_talk"
101
+ ```
245
102
 
246
- class Test
247
- include EasyTalk::Model
248
-
249
- define_schema do
250
- property :name, String
251
- end
252
- end
103
+ Then:
253
104
 
254
- puts Test.json_schema
105
+ ```bash
106
+ bundle install
255
107
  ```
256
108
 
257
- ## Quick Start
109
+ ---
110
+
111
+ ## Quick start
258
112
 
259
- ### Minimal Example
260
- Here's a basic example to get you started with EasyTalk:
113
+ <table>
114
+ <tr>
115
+ <th>EasyTalk Model</th>
116
+ <th>Generated JSON Schema</th>
117
+ </tr>
118
+ <tr>
119
+ <td>
261
120
 
262
121
  ```ruby
122
+ require "easy_talk"
123
+
263
124
  class User
264
125
  include EasyTalk::Model
265
126
 
266
127
  define_schema do
267
128
  title "User"
268
129
  description "A user of the system"
269
- property :name, String, description: "The user's name"
130
+
131
+ property :id, String
132
+ property :name, String, min_length: 2
270
133
  property :email, String, format: "email"
271
134
  property :age, Integer, minimum: 18
272
135
  end
273
136
  end
274
137
  ```
275
138
 
276
- ### Generated JSON Schema
277
- Calling `User.json_schema` will generate:
139
+ </td>
140
+ <td>
278
141
 
279
- ```ruby
142
+ ```json
280
143
  {
281
- "type" => "object",
282
- "title" => "User",
283
- "description" => "A user of the system",
284
- "properties" => {
285
- "name" => {
286
- "type" => "string",
287
- "description" => "The user's name"
288
- },
289
- "email" => {
290
- "type" => "string",
291
- "format" => "email"
292
- },
293
- "age" => {
294
- "type" => "integer",
295
- "minimum" => 18
296
- }
144
+ "type": "object",
145
+ "title": "User",
146
+ "description": "A user of the system",
147
+ "properties": {
148
+ "id": { "type": "string" },
149
+ "name": { "type": "string", "minLength": 2 },
150
+ "email": { "type": "string", "format": "email" },
151
+ "age": { "type": "integer", "minimum": 18 }
297
152
  },
298
- "required" => ["name", "email", "age"]
153
+ "required": ["id", "name", "email", "age"]
299
154
  }
300
155
  ```
301
156
 
302
- ### Basic Usage
303
- Creating and validating an instance of your model:
157
+ </td>
158
+ </tr>
159
+ </table>
304
160
 
305
161
  ```ruby
306
- user = User.new(name: "John Doe", email: "john@example.com", age: 25)
307
- user.valid? # => true (automatically validates based on schema constraints)
308
-
309
- user.age = 17
310
- user.valid? # => false (violates minimum: 18 constraint)
311
- user.errors[:age] # => ["must be greater than or equal to 18"]
162
+ User.json_schema # => Ruby Hash (JSON Schema)
163
+ user = User.new(name: "A") # invalid: min_length is 2
164
+ user.valid? # => false
165
+ user.errors # => ActiveModel::Errors
312
166
  ```
313
167
 
314
- ## Core Concepts
168
+ ---
315
169
 
316
- ### Schema Definition
317
- In EasyTalk, you define your schema by including the `EasyTalk::Model` module and using the `define_schema` method. This method takes a block where you can define the properties and constraints of your schema.
170
+ ## Property constraints
318
171
 
319
- ```ruby
320
- class MyModel
321
- include EasyTalk::Model
172
+ | Constraint | Applies to | Example |
173
+ |------------|-----------|---------|
174
+ | `min_length` / `max_length` | String | `property :name, String, min_length: 2, max_length: 50` |
175
+ | `minimum` / `maximum` | Integer, Float | `property :age, Integer, minimum: 18, maximum: 120` |
176
+ | `format` | String | `property :email, String, format: "email"` |
177
+ | `pattern` | String | `property :zip, String, pattern: '^\d{5}$'` |
178
+ | `enum` | Any | `property :status, String, enum: ["active", "inactive"]` |
179
+ | `min_items` / `max_items` | Array, Tuple | `property :tags, T::Array[String], min_items: 1` |
180
+ | `unique_items` | Array, Tuple | `property :ids, T::Array[Integer], unique_items: true` |
181
+ | `additional_items` | Tuple | `property :coords, T::Tuple[Float, Float], additional_items: false` |
182
+ | `optional` | Any | `property :nickname, String, optional: true` |
183
+ | `default` | Any | `property :role, String, default: "user"` |
184
+ | `description` | Any | `property :name, String, description: "Full name"` |
185
+ | `title` | Any | `property :name, String, title: "User Name"` |
322
186
 
323
- define_schema do
324
- title "My Model"
325
- description "Description of my model"
326
- property :some_property, String
327
- property :another_property, Integer
328
- end
329
- end
330
- ```
187
+ **Object-level constraints** (applied in `define_schema` block):
188
+ - `min_properties` / `max_properties` - Minimum/maximum number of properties
189
+ - `pattern_properties` - Schema for properties matching regex patterns
190
+ - `dependent_required` - Conditional property requirements
331
191
 
332
- ### Property Types
192
+ When `auto_validations` is enabled (default), these constraints automatically generate corresponding ActiveModel validations.
333
193
 
334
- #### Ruby Types
335
- EasyTalk supports standard Ruby types directly:
194
+ ---
336
195
 
337
- - `String`: String values
338
- - `Integer`: Integer values
339
- - `Float`: Floating-point numbers
340
- - `Date`: Date values
341
- - `DateTime`: Date and time values
196
+ ## Core concepts
342
197
 
343
- #### Sorbet-Style Types
344
- For complex types, EasyTalk uses Sorbet-style type notation:
198
+ ### Required vs optional vs nullable (don't get tricked)
345
199
 
346
- - `T::Boolean`: Boolean values (true/false)
347
- - `T::Array[Type]`: Arrays with items of a specific type
348
- - `T.nilable(Type)`: Type that can also be nil
200
+ JSON Schema distinguishes:
201
+ - **Optional**: property may be omitted (not in `required`)
202
+ - **Nullable**: property may be `null` (type includes `"null"`)
349
203
 
350
- #### Custom Types
351
- EasyTalk supports special composition types:
204
+ EasyTalk mirrors that precisely:
352
205
 
353
- - `T::AnyOf[Type1, Type2, ...]`: Value can match any of the specified schemas
354
- - `T::OneOf[Type1, Type2, ...]`: Value must match exactly one of the specified schemas
355
- - `T::AllOf[Type1, Type2, ...]`: Value must match all of the specified schemas
206
+ ```ruby
207
+ class Profile
208
+ include EasyTalk::Model
356
209
 
357
- ### Property Constraints
358
- Property constraints depend on the type of property. Some common constraints include:
210
+ define_schema do
211
+ # required, not nullable
212
+ property :name, String
359
213
 
360
- - `description`: A description of the property
361
- - `title`: A title for the property
362
- - `format`: A format hint for the property (e.g., "email", "date")
363
- - `enum`: A list of allowed values
364
- - `minimum`/`maximum`: Minimum/maximum values for numbers
365
- - `min_length`/`max_length`: Minimum/maximum length for strings
366
- - `pattern`: A regular expression pattern for strings
367
- - `min_items`/`max_items`: Minimum/maximum number of items for arrays
368
- - `unique_items`: Whether array items must be unique
214
+ # required, nullable (must exist, may be null)
215
+ property :age, T.nilable(Integer)
369
216
 
370
- ### Required vs Optional Properties
371
- By default, all properties defined in an EasyTalk model are required. You can make a property optional by specifying `optional: true`:
217
+ # optional, not nullable (may be omitted, but cannot be null if present)
218
+ property :nickname, String, optional: true
372
219
 
373
- ```ruby
374
- define_schema do
375
- property :name, String
376
- property :middle_name, String, optional: true
220
+ # optional + nullable (may be omitted OR null)
221
+ property :bio, T.nilable(String), optional: true
222
+ # or, equivalently:
223
+ nullable_optional_property :website, String
224
+ end
377
225
  end
378
226
  ```
379
227
 
380
- In this example, `name` is required but `middle_name` is optional.
381
-
382
- ### Automatic Validation Generation
383
- EasyTalk automatically generates ActiveModel validations from your schema constraints. This feature is enabled by default but can be configured:
228
+ By default, `T.nilable(Type)` makes a field **nullable but still required**.
229
+ If you want “nilable implies optional” behavior globally:
384
230
 
385
231
  ```ruby
386
- class User
387
- include EasyTalk::Model
388
-
389
- define_schema do
390
- property :name, String, min_length: 2, max_length: 50
391
- property :email, String, format: "email"
392
- property :age, Integer, minimum: 18, maximum: 120
393
- property :status, String, enum: ["active", "inactive", "pending"]
394
- end
395
- # Validations are automatically generated:
396
- # validates :name, presence: true, length: { minimum: 2, maximum: 50 }
397
- # validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
398
- # validates :age, presence: true, numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 120 }
399
- # validates :status, presence: true, inclusion: { in: ["active", "inactive", "pending"] }
232
+ EasyTalk.configure do |config|
233
+ config.nilable_is_optional = true
400
234
  end
401
-
402
- user = User.new(name: "Jo", email: "invalid-email", age: 17)
403
- user.valid? # => false
404
- user.errors.full_messages
405
- # => ["Name is too short (minimum is 2 characters)",
406
- # "Email is invalid",
407
- # "Age must be greater than or equal to 18"]
408
235
  ```
409
236
 
410
- ### Manual Validation Overrides
411
- You can still add manual validations alongside automatic ones:
237
+ ---
238
+
239
+ ### Nested models (and automatic instantiation)
240
+
241
+ Define nested objects as separate classes, then reference them:
412
242
 
413
243
  ```ruby
414
- class User
244
+ class Address
415
245
  include EasyTalk::Model
416
-
417
- # Custom validation in addition to automatic ones
418
- validates :email, uniqueness: true
419
- validate :complex_business_rule
420
-
246
+
421
247
  define_schema do
422
- property :name, String
423
- property :email, String, format: "email"
424
- property :age, Integer, minimum: 18
425
- end
426
-
427
- private
428
-
429
- def complex_business_rule
430
- # Custom validation logic
248
+ property :street, String
249
+ property :city, String
431
250
  end
432
251
  end
433
- ```
434
-
435
- ## Defining Schemas
436
252
 
437
- ### Basic Schema Structure
438
- A schema definition consists of a class that includes `EasyTalk::Model` and a `define_schema` block:
439
-
440
- ```ruby
441
- class Person
253
+ class User
442
254
  include EasyTalk::Model
443
255
 
444
256
  define_schema do
445
- title "Person"
446
257
  property :name, String
447
- property :age, Integer
258
+ property :address, Address
448
259
  end
449
260
  end
450
- ```
451
-
452
- ### Property Definitions
453
- Properties are defined using the `property` method, which takes a name, a type, and optional constraints:
454
-
455
- ```ruby
456
- property :name, String, description: "The person's name", title: "Full Name"
457
- property :age, Integer, minimum: 0, maximum: 120, description: "The person's age"
458
- ```
459
261
 
460
- ### Arrays and Collections
461
- Arrays can be defined using the `T::Array` type:
262
+ user = User.new(
263
+ name: "John",
264
+ address: { street: "123 Main St", city: "Boston" } # Hash becomes Address automatically
265
+ )
462
266
 
463
- ```ruby
464
- property :tags, T::Array[String], min_items: 1, unique_items: true
465
- property :scores, T::Array[Integer], description: "List of scores"
267
+ user.address.class # => Address
466
268
  ```
467
269
 
468
- You can also define arrays of complex types:
270
+ Nested models inside arrays work too:
469
271
 
470
272
  ```ruby
471
- property :addresses, T::Array[Address], description: "List of addresses"
472
- ```
473
-
474
- ### Constraints and Automatic Validations
475
- Constraints are added to properties and are used for both schema generation and automatic validation generation:
273
+ class Order
274
+ include EasyTalk::Model
476
275
 
477
- ```ruby
478
- define_schema do
479
- property :name, String, min_length: 2, max_length: 50
480
- property :email, String, format: "email"
481
- property :category, String, enum: ["A", "B", "C"]
482
- property :score, Float, minimum: 0.0, maximum: 100.0
483
- property :tags, T::Array[String], min_items: 1, max_items: 10
276
+ define_schema do
277
+ property :line_items, T::Array[Address], min_items: 1
278
+ end
484
279
  end
485
- # Automatically generates equivalent ActiveModel validations:
486
- # validates :name, presence: true, length: { minimum: 2, maximum: 50 }
487
- # validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
488
- # validates :category, presence: true, inclusion: { in: ["A", "B", "C"] }
489
- # validates :score, presence: true, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 }
490
- # validates :tags, presence: true, length: { minimum: 1, maximum: 10 }
491
280
  ```
492
281
 
493
- ### Supported Constraint-to-Validation Mappings
494
-
495
- | Constraint | Validation Generated |
496
- |------------|---------------------|
497
- | `min_length`, `max_length` | `length: { minimum: X, maximum: Y }` |
498
- | `minimum`, `maximum` | `numericality: { greater_than_or_equal_to: X, less_than_or_equal_to: Y }` |
499
- | `format: "email"` | `format: { with: URI::MailTo::EMAIL_REGEXP }` |
500
- | `format: "url"` or `format: "uri"` | `format: { with: URI::regexp }` |
501
- | `pattern: /regex/` | `format: { with: /regex/ }` |
502
- | `enum: [...]` | `inclusion: { in: [...] }` |
503
- | `min_items`, `max_items` (arrays) | `length: { minimum: X, maximum: Y }` |
504
- | `optional: true` | Skips presence validation |
505
- | `T.nilable(Type)` | Allows nil values, skips presence validation |
282
+ ---
506
283
 
507
- ### Additional Properties
508
- By default, EasyTalk models do not allow additional properties beyond those defined in the schema. You can change this behavior using the `additional_properties` keyword:
284
+ ### Tuple arrays (fixed-position types)
509
285
 
510
- ```ruby
511
- define_schema do
512
- property :name, String
513
- additional_properties true
514
- end
515
- ```
286
+ Use `T::Tuple` for arrays where each position has a specific type (e.g., coordinates, CSV rows, database records):
516
287
 
517
- With `additional_properties true`, you can add arbitrary properties to your model instances:
288
+ <table>
289
+ <tr>
290
+ <th>EasyTalk Model</th>
291
+ <th>Generated JSON Schema</th>
292
+ </tr>
293
+ <tr>
294
+ <td>
518
295
 
519
296
  ```ruby
520
- company = Company.new
521
- company.name = "Acme Corp" # Defined property
522
- company.location = "New York" # Additional property
523
- company.employee_count = 100 # Additional property
524
- ```
525
-
526
- ### Property Naming
527
- You can configure the naming strategy for properties globally or per schema:
297
+ class GeoLocation
298
+ include EasyTalk::Model
528
299
 
529
- ```ruby
530
- EasyTalk.configure do |config|
531
- config.property_naming_strategy = :snake_case # Options: :identity, :snake_case, :camel_case, :pascal_case
300
+ define_schema do
301
+ property :name, String
302
+ # Fixed: [latitude, longitude]
303
+ property :coordinates, T::Tuple[Float, Float]
304
+ end
532
305
  end
533
306
 
534
- define_schema do
535
- property_naming_strategy :camel_case # Overrides global setting for this schema
536
- property :name, String
537
- end
307
+ location = GeoLocation.new(
308
+ name: 'Office',
309
+ coordinates: [40.7128, -74.0060]
310
+ )
538
311
  ```
539
312
 
540
- This affects how property names are represented in the generated JSON Schema.
541
- Additionally, names can be overridden per property:
313
+ </td>
314
+ <td>
542
315
 
543
- ```ruby
544
- property :first_name, String, as: "firstName" # Overrides global naming strategy
316
+ ```json
317
+ {
318
+ "properties": {
319
+ "coordinates": {
320
+ "type": "array",
321
+ "items": [
322
+ { "type": "number" },
323
+ { "type": "number" }
324
+ ]
325
+ }
326
+ }
327
+ }
545
328
  ```
546
329
 
547
- ## Schema Composition
330
+ </td>
331
+ </tr>
332
+ </table>
548
333
 
549
- ### Using T::AnyOf
550
- The `T::AnyOf` type allows a property to match any of the specified schemas:
334
+ **Mixed-type tuples:**
551
335
 
552
336
  ```ruby
553
- class Payment
337
+ class DataRow
554
338
  include EasyTalk::Model
555
339
 
556
340
  define_schema do
557
- property :details, T::AnyOf[CreditCard, Paypal, BankTransfer]
341
+ # Fixed: [name, age, active]
342
+ property :row, T::Tuple[String, Integer, T::Boolean]
558
343
  end
559
344
  end
560
345
  ```
561
346
 
562
- ### Using T::OneOf
563
- The `T::OneOf` type requires a property to match exactly one of the specified schemas:
347
+ **Controlling extra items:**
564
348
 
565
349
  ```ruby
566
- class Contact
567
- include EasyTalk::Model
350
+ define_schema do
351
+ # Reject extra items (strict tuple)
352
+ property :rgb, T::Tuple[Integer, Integer, Integer], additional_items: false
568
353
 
569
- define_schema do
570
- property :contact, T::OneOf[PhoneContact, EmailContact]
571
- end
354
+ # Allow extra items of specific type
355
+ property :header_values, T::Tuple[String], additional_items: Integer
356
+
357
+ # Allow any extra items (default)
358
+ property :flexible, T::Tuple[String, Integer]
572
359
  end
573
360
  ```
574
361
 
575
- ### Using T::AllOf
576
- The `T::AllOf` type requires a property to match all of the specified schemas:
362
+ **Tuple validation:**
577
363
 
578
364
  ```ruby
579
- class VehicleRegistration
580
- include EasyTalk::Model
581
-
582
- define_schema do
583
- compose T::AllOf[VehicleIdentification, OwnerInfo, RegistrationDetails]
584
- end
585
- end
365
+ model = GeoLocation.new(coordinates: [40.7, "invalid"])
366
+ model.valid? # => false
367
+ model.errors[:coordinates]
368
+ # => ["item at index 1 must be a Float"]
586
369
  ```
587
370
 
588
- ### Array Composition
589
- Composition types can be combined with arrays to define collections where each item must match one of several schemas:
371
+ ---
372
+
373
+ ### Composition (AnyOf / OneOf / AllOf)
590
374
 
591
375
  ```ruby
592
376
  class ProductA
@@ -601,1814 +385,522 @@ class ProductB
601
385
  include EasyTalk::Model
602
386
  define_schema do
603
387
  property :sku, String
604
- property :digital_url, String
388
+ property :color, String
605
389
  end
606
390
  end
607
391
 
608
- class Order
392
+ class Cart
609
393
  include EasyTalk::Model
610
394
 
611
395
  define_schema do
612
- property :order_id, String
613
- # Each item in the array must match exactly one of the product schemas
614
- property :items, T::Array[T::OneOf[ProductA, ProductB]]
396
+ property :items, T::Array[T::AnyOf[ProductA, ProductB]]
615
397
  end
616
398
  end
617
399
  ```
618
400
 
619
- This generates a JSON Schema where the `items` array validates each element against `oneOf`:
401
+ ---
620
402
 
621
- ```json
622
- {
623
- "properties": {
624
- "items": {
625
- "type": "array",
626
- "items": {
627
- "oneOf": [
628
- { "type": "object", "properties": { "sku": {...}, "weight": {...} }, ... },
629
- { "type": "object", "properties": { "sku": {...}, "digital_url": {...} }, ... }
630
- ]
631
- }
632
- }
633
- }
634
- }
635
- ```
403
+ ## Validations
636
404
 
637
- You can use any composition type with arrays:
638
- - `T::Array[T::OneOf[A, B]]` - each item matches exactly one schema
639
- - `T::Array[T::AnyOf[A, B]]` - each item matches one or more schemas
640
- - `T::Array[T::AllOf[A, B]]` - each item matches all schemas
405
+ ### Automatic validations (default)
641
406
 
642
- ### Complex Compositions
643
- You can combine composition types to create complex schemas:
407
+ EasyTalk can generate ActiveModel validations from constraints:
644
408
 
645
409
  ```ruby
646
- class ComplexObject
647
- include EasyTalk::Model
410
+ EasyTalk.configure do |config|
411
+ config.auto_validations = true
412
+ end
413
+ ```
648
414
 
649
- define_schema do
650
- property :basic_info, BaseInfo
651
- property :specific_details, T::OneOf[DetailTypeA, DetailTypeB]
652
- property :metadata, T::AnyOf[AdminMetadata, UserMetadata, nil]
653
- end
415
+ Disable globally:
416
+
417
+ ```ruby
418
+ EasyTalk.configure do |config|
419
+ config.auto_validations = false
654
420
  end
655
421
  ```
656
422
 
657
- ### Reusing Models
658
- Models can reference other models to create hierarchical schemas:
423
+ When auto validations are off, you can still write validations manually:
659
424
 
660
425
  ```ruby
661
- class Address
426
+ class User
662
427
  include EasyTalk::Model
663
-
428
+
429
+ validates :name, presence: true, length: { minimum: 2 }
430
+
664
431
  define_schema do
665
- property :street, String
666
- property :city, String
667
- property :state, String
668
- property :zip, String
432
+ property :name, String, min_length: 2
669
433
  end
670
434
  end
435
+ ```
671
436
 
672
- class User
437
+ ### Per-model validation control
438
+
439
+ ```ruby
440
+ class LegacyModel
673
441
  include EasyTalk::Model
674
-
675
- define_schema do
676
- property :name, String
677
- property :address, Address
442
+
443
+ define_schema(validations: false) do
444
+ property :data, String, min_length: 1 # no validation generated
678
445
  end
679
446
  end
680
447
  ```
681
448
 
682
- ## ActiveModel Integration
683
-
684
- ### Enhanced Validation System
685
- EasyTalk models include comprehensive ActiveModel validation support with automatic generation:
449
+ ### Per-property validation control
686
450
 
687
451
  ```ruby
688
452
  class User
689
453
  include EasyTalk::Model
690
-
691
- # Manual validations work alongside automatic ones
692
- validates :age, comparison: { greater_than: 21 } # Additional business rule
693
- validates :height, numericality: { greater_than: 0 } # Overrides auto-validation
694
-
454
+
695
455
  define_schema do
696
- property :name, String, min_length: 2 # Auto-generates presence + length validations
697
- property :age, Integer, minimum: 18 # Auto-generates presence + numericality validations
698
- property :height, Float # Auto-generates presence validation (overridden above)
456
+ property :name, String, min_length: 2
457
+ property :legacy_field, String, validate: false
699
458
  end
700
459
  end
701
460
  ```
702
461
 
703
- ### Error Handling
704
- You can access validation errors using the standard ActiveModel methods:
462
+ ### Validation adapters
463
+
464
+ EasyTalk uses a pluggable adapter system:
705
465
 
706
466
  ```ruby
707
- user = User.new(name: "J", age: 18, height: -5.9)
708
- user.valid? # => false
709
- user.errors[:name] # => ["is too short (minimum is 2 characters)"]
710
- user.errors[:age] # => ["must be greater than 21"] # Custom validation
711
- user.errors[:height] # => ["must be greater than 0"] # Overridden validation
467
+ EasyTalk.configure do |config|
468
+ config.validation_adapter = :active_model # default
469
+ # config.validation_adapter = :none # disable validation generation
470
+ end
712
471
  ```
713
472
 
714
- ### Standardized Error Formatting
715
-
716
- EasyTalk provides multiple output formats for validation errors, making it easy to build consistent API responses.
473
+ ---
717
474
 
718
- #### Available Formats
475
+ ## Error formatting
719
476
 
720
- | Format | Description | Use Case |
721
- |--------|-------------|----------|
722
- | `:flat` | Simple array of field/message/code objects | General purpose APIs |
723
- | `:json_pointer` | Array with JSON Pointer (RFC 6901) paths | JSON Schema validation |
724
- | `:rfc7807` | RFC 7807 Problem Details format | Standards-compliant APIs |
725
- | `:jsonapi` | JSON:API specification error format | JSON:API implementations |
726
-
727
- #### Instance Methods
728
-
729
- Every EasyTalk model includes convenient methods for error formatting:
477
+ Instance helpers:
730
478
 
731
479
  ```ruby
732
- user = User.new(name: "", email: "invalid")
733
- user.valid?
734
-
735
- # Use default format (configurable globally)
736
- user.validation_errors
737
- # => [{"field" => "name", "message" => "can't be blank", "code" => "blank"}, ...]
738
-
739
- # Flat format
740
480
  user.validation_errors_flat
741
- # => [{"field" => "name", "message" => "can't be blank", "code" => "blank"}]
742
-
743
- # JSON Pointer format
744
481
  user.validation_errors_json_pointer
745
- # => [{"pointer" => "/properties/name", "message" => "can't be blank", "code" => "blank"}]
746
-
747
- # RFC 7807 Problem Details
748
482
  user.validation_errors_rfc7807
749
- # => {
750
- # "type" => "about:blank#validation-error",
751
- # "title" => "Validation Failed",
752
- # "status" => 422,
753
- # "detail" => "The request contains invalid parameters",
754
- # "errors" => [...]
755
- # }
756
-
757
- # JSON:API format
758
483
  user.validation_errors_jsonapi
759
- # => {
760
- # "errors" => [
761
- # {"status" => "422", "source" => {"pointer" => "/data/attributes/name"}, ...}
762
- # ]
763
- # }
764
484
  ```
765
485
 
766
- #### Direct API Usage
767
-
768
- You can also format errors directly using the `ErrorFormatter` module:
486
+ Format directly:
769
487
 
770
488
  ```ruby
771
489
  EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: "User Validation Failed")
772
490
  ```
773
491
 
774
- #### Configuration
775
-
776
- Configure error formatting globally:
492
+ Global defaults:
777
493
 
778
494
  ```ruby
779
495
  EasyTalk.configure do |config|
780
- config.default_error_format = :rfc7807 # Default format for validation_errors
781
- config.error_type_base_uri = 'https://api.example.com/errors' # Base URI for RFC 7807
782
- config.include_error_codes = true # Include error codes in output
496
+ config.default_error_format = :rfc7807
497
+ config.error_type_base_uri = "https://api.example.com/errors"
498
+ config.include_error_codes = true
783
499
  end
784
500
  ```
785
501
 
786
- ### Model Attributes
787
- EasyTalk models provide getters and setters for all defined properties:
502
+ ---
788
503
 
789
- ```ruby
790
- user = User.new
791
- user.name = "John"
792
- user.age = 30
793
- puts user.name # => "John"
794
- ```
504
+ ## Schema-only mode
795
505
 
796
- You can also initialize a model with a hash of attributes, including nested EasyTalk models:
506
+ If you want schema generation and attribute accessors **without** ActiveModel validation:
797
507
 
798
508
  ```ruby
799
- user = User.new(name: "John", age: 30, height: 5.9)
509
+ class ApiContract
510
+ include EasyTalk::Schema
800
511
 
801
- # NEW in v2.0.0: Automatic nested model instantiation
802
- class Address
803
- include EasyTalk::Model
804
512
  define_schema do
805
- property :street, String
806
- property :city, String
513
+ title "API Contract"
514
+ property :name, String, min_length: 2
515
+ property :age, Integer, minimum: 0
807
516
  end
808
517
  end
809
518
 
810
- class User
811
- include EasyTalk::Model
812
- define_schema do
813
- property :name, String
814
- property :address, Address
815
- end
816
- end
519
+ ApiContract.json_schema
520
+ contract = ApiContract.new(name: "Test", age: 25)
817
521
 
818
- # Hash attributes automatically instantiate nested models
819
- user = User.new(
820
- name: "John",
821
- address: { street: "123 Main St", city: "Boston" }
822
- )
823
- user.address.class # => Address (automatically instantiated)
824
- user.address.street # => "123 Main St"
522
+ # No validations available:
523
+ # contract.valid? # => NoMethodError
825
524
  ```
826
525
 
827
- ## Advanced Features
526
+ Use this for documentation, OpenAPI generation, or when validation happens elsewhere.
828
527
 
829
- ### Schema-Only Mode (EasyTalk::Schema)
528
+ ---
830
529
 
831
- For scenarios where you need JSON Schema generation without ActiveModel validations, use `EasyTalk::Schema` instead of `EasyTalk::Model`. This is ideal for:
530
+ ## RubyLLM Integration
832
531
 
833
- - API documentation and OpenAPI spec generation
834
- - Schema-first design where validation happens elsewhere
835
- - High-performance scenarios where validation overhead is unwanted
836
- - Generating schemas for external systems
532
+ EasyTalk integrates seamlessly with [RubyLLM](https://github.com/crmne/ruby_llm) for structured outputs and tool definitions.
533
+
534
+ ### Structured Outputs
535
+
536
+ Use any EasyTalk model with RubyLLM's `with_schema` to get structured JSON responses:
837
537
 
838
538
  ```ruby
839
- class ApiContract
840
- include EasyTalk::Schema # Not EasyTalk::Model
539
+ class Recipe
540
+ include EasyTalk::Model
841
541
 
842
542
  define_schema do
843
- title 'API Contract'
844
- description 'A schema-only contract'
845
- property :name, String, min_length: 2
846
- property :age, Integer, minimum: 0
543
+ description "A cooking recipe"
544
+ property :name, String, description: "Name of the dish"
545
+ property :ingredients, T::Array[String], description: "List of ingredients"
546
+ property :prep_time_minutes, Integer, description: "Preparation time in minutes"
847
547
  end
848
548
  end
849
549
 
850
- # Schema generation works the same
851
- ApiContract.json_schema
852
- # => {"type" => "object", "title" => "API Contract", ...}
853
-
854
- # Instances can be created and accessed
855
- contract = ApiContract.new(name: 'Test', age: 25)
856
- contract.name # => 'Test'
550
+ chat = RubyLLM.chat.with_schema(Recipe)
551
+ response = chat.ask("Give me a simple pasta recipe")
857
552
 
858
- # But no validation methods are available
859
- contract.valid? # => NoMethodError
860
- contract.errors # => NoMethodError
553
+ # RubyLLM returns parsed JSON - instantiate with EasyTalk model
554
+ recipe = Recipe.new(response.content)
555
+ recipe.name # => "Spaghetti Aglio e Olio"
556
+ recipe.ingredients # => ["spaghetti", "garlic", "olive oil", ...]
861
557
  ```
862
558
 
863
- #### Key Differences from EasyTalk::Model
559
+ ### Tools
864
560
 
865
- | Feature | EasyTalk::Model | EasyTalk::Schema |
866
- |---------|-----------------|------------------|
867
- | JSON Schema generation | Yes | Yes |
868
- | Property accessors | Yes | Yes |
869
- | Nested model instantiation | Yes | Yes |
870
- | ActiveModel::Validations | Yes | No |
871
- | `valid?` / `errors` | Yes | No |
872
- | Validation adapters | Yes | N/A |
873
- | Error formatting | Yes | No |
874
-
875
- #### When to Use Each
876
-
877
- - **Use `EasyTalk::Model`** when you need runtime validation of data (form inputs, API requests, user data)
878
- - **Use `EasyTalk::Schema`** when you only need the schema definition (documentation, code generation, external validation)
879
-
880
- ### LLM Function Generation
881
- EasyTalk provides a helper method for generating OpenAI function specifications:
561
+ Create LLM tools by inheriting from `RubyLLM::Tool` and including `EasyTalk::Model`:
882
562
 
883
563
  ```ruby
884
- class Weather
564
+ class Weather < RubyLLM::Tool
885
565
  include EasyTalk::Model
886
-
566
+
887
567
  define_schema do
888
- title "GetWeather"
889
- description "Get the current weather in a given location"
890
- property :location, String, description: "The city and state, e.g. San Francisco, CA"
891
- property :unit, String, enum: ["celsius", "fahrenheit"], default: "fahrenheit"
568
+ description 'Gets current weather for a location'
569
+ property :latitude, String, description: 'Latitude (e.g., 52.5200)'
570
+ property :longitude, String, description: 'Longitude (e.g., 13.4050)'
571
+ end
572
+
573
+ def execute(latitude:, longitude:)
574
+ # Fetch weather data from API...
575
+ { temperature: 22, conditions: "sunny" }
892
576
  end
893
577
  end
894
578
 
895
- function_spec = EasyTalk::Tools::FunctionBuilder.new(Weather)
579
+ chat = RubyLLM.chat.with_tool(Weather)
580
+ response = chat.ask("What's the weather in Berlin?")
896
581
  ```
897
582
 
898
- This generates a function specification compatible with OpenAI's function calling API.
583
+ This pattern gives you:
584
+ - Full access to `RubyLLM::Tool` features (`halt`, `call`, etc.)
585
+ - EasyTalk's schema DSL for parameter definitions
586
+ - Automatic JSON Schema generation for the LLM
899
587
 
900
- ### Schema Transformation
901
- You can transform EasyTalk schemas into various formats:
588
+ ---
589
+
590
+ ## Configuration highlights
902
591
 
903
592
  ```ruby
904
- # Get Ruby hash representation
905
- schema_hash = User.schema
593
+ EasyTalk.configure do |config|
594
+ # Schema behavior
595
+ config.default_additional_properties = false
596
+ config.nilable_is_optional = false
597
+ config.schema_version = :none
598
+ config.schema_id = nil
599
+ config.use_refs = false
600
+ config.base_schema_uri = nil # Base URI for auto-generating $id
601
+ config.auto_generate_ids = false # Auto-generate $id from base_schema_uri
602
+ config.prefer_external_refs = false # Use external URI in $ref when available
603
+ config.property_naming_strategy = :identity # :snake_case, :camel_case, :pascal_case
906
604
 
907
- # Get JSON Schema representation
908
- json_schema = User.json_schema
605
+ # Validations
606
+ config.auto_validations = true
607
+ config.validation_adapter = :active_model
909
608
 
910
- # Convert to JSON string
911
- json_string = User.json_schema.to_json
609
+ # Error formatting
610
+ config.default_error_format = :flat # :flat, :json_pointer, :rfc7807, :jsonapi
611
+ config.error_type_base_uri = "about:blank"
612
+ config.include_error_codes = true
613
+ end
912
614
  ```
913
615
 
914
- ### Type Checking and Validation
915
- EasyTalk performs basic type checking during schema definition:
916
-
917
- ```ruby
918
- # This will raise an error because "minimum" should be used with numeric types
919
- property :name, String, minimum: 1 # Error!
920
-
921
- # This will raise an error because enum values must match the property type
922
- property :age, Integer, enum: ["young", "old"] # Error!
923
- ```
616
+ ---
924
617
 
925
- ### Custom Type Builders
618
+ ## Advanced topics
926
619
 
927
- EasyTalk provides a type registry that allows you to register custom types with their corresponding schema builders.
620
+ For more detailed documentation, see the [full API reference on RubyDoc](https://rubydoc.info/gems/easy_talk).
928
621
 
929
- #### Registering Custom Types
622
+ ### JSON Schema drafts, `$id`, and `$ref`
930
623
 
931
- Register types in your configuration:
624
+ EasyTalk can emit `$schema` for multiple drafts (Draft-04 through 2020-12), supports `$id`, and can use `$ref`/`$defs` for reusable definitions:
932
625
 
933
626
  ```ruby
934
627
  EasyTalk.configure do |config|
935
- config.register_type(Money, MoneySchemaBuilder)
628
+ config.schema_version = :draft202012
629
+ config.schema_id = "https://example.com/schemas/user.json"
630
+ config.use_refs = true # Use $ref/$defs for nested models
936
631
  end
937
632
  ```
938
633
 
939
- Or register directly with the registry:
940
-
941
- ```ruby
942
- EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
943
- ```
634
+ #### External schema references
944
635
 
945
- #### Creating a Custom Builder
636
+ Use external URIs in `$ref` for modular, reusable schemas:
946
637
 
947
- Custom builders extend `BaseBuilder` and implement the schema generation logic:
638
+ <table>
639
+ <tr>
640
+ <th>EasyTalk Model</th>
641
+ <th>Generated JSON Schema</th>
642
+ </tr>
643
+ <tr>
644
+ <td>
948
645
 
949
646
  ```ruby
950
- class MoneySchemaBuilder < EasyTalk::Builders::BaseBuilder
951
- VALID_OPTIONS = {
952
- currency: { type: T.nilable(String), key: :currency }
953
- }.freeze
954
-
955
- def initialize(name, options = {})
956
- super(name, { type: 'object' }, options, VALID_OPTIONS)
957
- end
958
-
959
- def build
960
- schema.merge(
961
- properties: {
962
- amount: { type: 'number' },
963
- currency: { type: 'string', default: options[:currency] || 'USD' }
964
- },
965
- required: %w[amount currency]
966
- )
967
- end
647
+ EasyTalk.configure do |config|
648
+ config.use_refs = true
649
+ config.prefer_external_refs = true
650
+ config.base_schema_uri = 'https://example.com/schemas'
651
+ config.auto_generate_ids = true
968
652
  end
969
653
 
970
- # Register and use
971
- EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
972
-
973
- class Order
654
+ class Address
974
655
  include EasyTalk::Model
975
656
 
976
657
  define_schema do
977
- property :total, Money, currency: 'EUR'
978
- end
979
- end
980
- ```
981
-
982
- #### Collection Type Builders
983
-
984
- For types that wrap other types (like arrays), use the `collection: true` option:
985
-
986
- ```ruby
987
- EasyTalk::Builders::Registry.register(
988
- CustomCollection,
989
- CustomCollectionBuilder,
990
- collection: true
991
- )
992
- ```
993
-
994
- Collection builders receive `(name, inner_type, constraints)` instead of `(name, constraints)`.
995
-
996
- #### Overriding Built-in Types
997
-
998
- You can override built-in type builders:
999
-
1000
- ```ruby
1001
- class EnhancedStringBuilder < EasyTalk::Builders::StringBuilder
1002
- def build
1003
- result = super
1004
- result[:custom_extension] = true
1005
- result
1006
- end
1007
- end
1008
-
1009
- EasyTalk::Builders::Registry.register(String, EnhancedStringBuilder)
1010
- ```
1011
-
1012
- #### Registry API
1013
-
1014
- ```ruby
1015
- # Check if a type is registered
1016
- EasyTalk::Builders::Registry.registered?(Money) # => true
1017
-
1018
- # List all registered types
1019
- EasyTalk::Builders::Registry.registered_types
1020
-
1021
- # Unregister a type
1022
- EasyTalk::Builders::Registry.unregister(Money)
1023
-
1024
- # Reset registry to defaults
1025
- EasyTalk::Builders::Registry.reset!
1026
- ```
1027
-
1028
- ### Type Introspection
1029
-
1030
- EasyTalk provides a `TypeIntrospection` module for reliable type detection, useful when building custom type builders:
1031
-
1032
- ```ruby
1033
- # Check type categories
1034
- EasyTalk::TypeIntrospection.boolean_type?(T::Boolean) # => true
1035
- EasyTalk::TypeIntrospection.typed_array?(T::Array[String]) # => true
1036
- EasyTalk::TypeIntrospection.nilable_type?(T.nilable(String)) # => true
1037
- EasyTalk::TypeIntrospection.primitive_type?(Integer) # => true
1038
-
1039
- # Get JSON Schema type string
1040
- EasyTalk::TypeIntrospection.json_schema_type(Integer) # => 'integer'
1041
- EasyTalk::TypeIntrospection.json_schema_type(Float) # => 'number'
1042
-
1043
- # Extract inner type from nilable
1044
- EasyTalk::TypeIntrospection.extract_inner_type(T.nilable(String)) # => String
1045
- ```
1046
-
1047
- ## Configuration
1048
-
1049
- ### Global Settings
1050
- You can configure EasyTalk globally:
1051
-
1052
- ```ruby
1053
- EasyTalk.configure do |config|
1054
- # Schema behavior options
1055
- config.default_additional_properties = false # Control additional properties on all models
1056
- config.nilable_is_optional = false # Makes T.nilable properties also optional
1057
- config.schema_version = :none # JSON Schema version for $schema keyword
1058
- # Options: :none, :draft202012, :draft201909, :draft7, :draft6, :draft4
1059
- config.schema_id = nil # Base URI for $id keyword (nil = no $id)
1060
- config.use_refs = false # Use $ref for nested models instead of inlining
1061
- config.property_naming_strategy = :camel_case # Options: :identity (default), :snake_case, :camel_case, :pascal_case
1062
-
1063
- # Validation options
1064
- config.auto_validations = true # Automatically generate ActiveModel validations
1065
- config.validation_adapter = :active_model # Validation backend (:active_model, :none, or custom)
1066
-
1067
- # Error formatting options
1068
- config.default_error_format = :flat # Default format (:flat, :json_pointer, :rfc7807, :jsonapi)
1069
- config.error_type_base_uri = 'about:blank' # Base URI for RFC 7807 error types
1070
- config.include_error_codes = true # Include error codes in formatted output
1071
- end
1072
- ```
1073
-
1074
- ### Automatic Validation Configuration
1075
- The new `auto_validations` option (enabled by default) automatically generates ActiveModel validations from your schema constraints:
1076
-
1077
- ```ruby
1078
- # Disable automatic validations globally
1079
- EasyTalk.configure do |config|
1080
- config.auto_validations = false
1081
- end
1082
-
1083
- # Now you must manually define validations
1084
- class User
1085
- include EasyTalk::Model
1086
-
1087
- validates :name, presence: true, length: { minimum: 2 }
1088
- validates :age, presence: true, numericality: { greater_than_or_equal_to: 18 }
1089
-
1090
- define_schema do
1091
- property :name, String, min_length: 2
1092
- property :age, Integer, minimum: 18
1093
- end
1094
- end
1095
- ```
1096
-
1097
- ### Per-Model Configuration
1098
- You can configure additional properties for individual models:
1099
-
1100
- ```ruby
1101
- class User
1102
- include EasyTalk::Model
1103
-
1104
- define_schema do
1105
- title "User"
1106
- additional_properties true # Allow arbitrary additional properties on this model
1107
- property :name, String
1108
- property :email, String, format: "email"
1109
- end
1110
- end
1111
- ```
1112
-
1113
- ### Validation Adapters
1114
-
1115
- EasyTalk uses a pluggable validation adapter system that allows you to customize how validations are generated from schema constraints.
1116
-
1117
- #### Built-in Adapters
1118
-
1119
- | Adapter | Description |
1120
- |---------|-------------|
1121
- | `:active_model` | Default. Generates ActiveModel validations from schema constraints |
1122
- | `:none` | Skips validation generation entirely (schema-only mode) |
1123
-
1124
- #### Global Adapter Configuration
1125
-
1126
- ```ruby
1127
- EasyTalk.configure do |config|
1128
- config.validation_adapter = :none # Disable all automatic validations
1129
- end
1130
- ```
1131
-
1132
- #### Per-Model Validation Control
1133
-
1134
- Disable validations for a specific model while keeping them enabled globally:
1135
-
1136
- ```ruby
1137
- class LegacyModel
1138
- include EasyTalk::Model
1139
-
1140
- define_schema(validations: false) do
1141
- property :data, String, min_length: 1 # No validation generated
1142
- end
1143
- end
1144
- ```
1145
-
1146
- #### Per-Property Validation Control
1147
-
1148
- Disable validation for specific properties:
1149
-
1150
- ```ruby
1151
- class User
1152
- include EasyTalk::Model
1153
-
1154
- define_schema do
1155
- property :name, String, min_length: 2 # Validation generated
1156
- property :legacy_field, String, validate: false # No validation for this property
1157
- end
1158
- end
1159
- ```
1160
-
1161
- #### Custom Validation Adapters
1162
-
1163
- Create custom adapters for specialized validation needs:
1164
-
1165
- ```ruby
1166
- class MyCustomAdapter < EasyTalk::ValidationAdapters::Base
1167
- def self.build_validations(klass, property_name, type, constraints)
1168
- # Custom validation logic
1169
- end
1170
- end
1171
-
1172
- # Register the adapter
1173
- EasyTalk::ValidationAdapters::Registry.register(:custom, MyCustomAdapter)
1174
-
1175
- # Use it globally
1176
- EasyTalk.configure do |config|
1177
- config.validation_adapter = :custom
1178
- end
1179
- ```
1180
-
1181
- ## Examples
1182
-
1183
- ### User Registration (with Auto-Validations)
1184
-
1185
- ```ruby
1186
- class User
1187
- include EasyTalk::Model
1188
-
1189
- # Additional custom validations beyond automatic ones
1190
- validates :email, uniqueness: true
1191
- validates :password, confirmation: true
1192
-
1193
- define_schema do
1194
- title "User Registration"
1195
- description "User registration information"
1196
- property :name, String, min_length: 2, max_length: 100, description: "User's full name"
1197
- property :email, String, format: "email", description: "User's email address"
1198
- property :password, String, min_length: 8, max_length: 128, description: "User's password"
1199
- property :notify, T::Boolean, optional: true, description: "Whether to send notifications"
1200
- end
1201
- # Auto-generated validations:
1202
- # validates :name, presence: true, length: { minimum: 2, maximum: 100 }
1203
- # validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
1204
- # validates :password, presence: true, length: { minimum: 8, maximum: 128 }
1205
- # validates :notify, inclusion: { in: [true, false] } - only if present (optional: true)
1206
- end
1207
-
1208
- # Usage with automatic validation
1209
- user = User.new(
1210
- name: "John Doe",
1211
- email: "john@example.com",
1212
- password: "secretpassword123",
1213
- notify: true
1214
- )
1215
- user.valid? # => true (assuming email is unique)
1216
-
1217
- # Invalid data triggers auto-generated validations
1218
- invalid_user = User.new(
1219
- name: "J", # Too short
1220
- email: "invalid-email", # Invalid format
1221
- password: "123" # Too short
1222
- )
1223
- invalid_user.valid? # => false
1224
- invalid_user.errors.full_messages
1225
- # => ["Name is too short (minimum is 2 characters)",
1226
- # "Email is invalid",
1227
- # "Password is too short (minimum is 8 characters)"]
1228
- ```
1229
-
1230
- ### Payment Processing
1231
-
1232
- ```ruby
1233
- class CreditCard
1234
- include EasyTalk::Model
1235
-
1236
- define_schema do
1237
- property :CardNumber, String
1238
- property :CardType, String, enum: %w[Visa MasterCard AmericanExpress]
1239
- property :CardExpMonth, Integer, minimum: 1, maximum: 12
1240
- property :CardExpYear, Integer, minimum: Date.today.year, maximum: Date.today.year + 10
1241
- property :CardCVV, String, pattern: '^[0-9]{3,4}$'
1242
- additional_properties false
1243
- end
1244
- end
1245
-
1246
- class Paypal
1247
- include EasyTalk::Model
1248
-
1249
- define_schema do
1250
- property :PaypalEmail, String, format: 'email'
1251
- property :PaypalPasswordEncrypted, String
1252
- additional_properties false
1253
- end
1254
- end
1255
-
1256
- class BankTransfer
1257
- include EasyTalk::Model
1258
-
1259
- define_schema do
1260
- property :BankName, String
1261
- property :AccountNumber, String
1262
- property :RoutingNumber, String
1263
- property :AccountType, String, enum: %w[Checking Savings]
1264
- additional_properties false
1265
- end
1266
- end
1267
-
1268
- class Payment
1269
- include EasyTalk::Model
1270
-
1271
- define_schema do
1272
- title 'Payment'
1273
- description 'Payment info'
1274
- property :PaymentMethod, String, enum: %w[CreditCard Paypal BankTransfer]
1275
- property :Details, T::AnyOf[CreditCard, Paypal, BankTransfer]
1276
- end
1277
- end
1278
- ```
1279
-
1280
- ### Complex Object Hierarchies
1281
-
1282
- ```ruby
1283
- class Address
1284
- include EasyTalk::Model
1285
-
1286
- define_schema do
1287
- property :street, String
1288
- property :city, String
1289
- property :state, String
1290
- property :zip, String, pattern: '^[0-9]{5}(?:-[0-9]{4})?$'
1291
- end
1292
- end
1293
-
1294
- class Employee
1295
- include EasyTalk::Model
1296
-
1297
- define_schema do
1298
- title 'Employee'
1299
- description 'Company employee'
1300
- property :name, String, title: 'Full Name'
1301
- property :gender, String, enum: %w[male female other]
1302
- property :department, T.nilable(String)
1303
- property :hire_date, Date
1304
- property :active, T::Boolean, default: true
1305
- property :addresses, T.nilable(T::Array[Address])
1306
- end
1307
- end
1308
-
1309
- class Company
1310
- include EasyTalk::Model
1311
-
1312
- define_schema do
1313
- title 'Company'
1314
- property :name, String
1315
- property :employees, T::Array[Employee], title: 'Company Employees', description: 'A list of company employees'
1316
- end
1317
- end
1318
- ```
1319
-
1320
- ### API Integration
1321
-
1322
- ```ruby
1323
- # app/controllers/api/users_controller.rb
1324
- class Api::UsersController < ApplicationController
1325
- def create
1326
- schema = User.json_schema
1327
-
1328
- # Validate incoming request against the schema
1329
- validation_result = JSONSchemer.schema(schema).valid?(params.to_json)
1330
-
1331
- if validation_result
1332
- user = User.new(user_params)
1333
- if user.save
1334
- render json: user, status: :created
1335
- else
1336
- render json: { errors: user.errors }, status: :unprocessable_entity
1337
- end
1338
- else
1339
- render json: { errors: "Invalid request" }, status: :bad_request
1340
- end
1341
- end
1342
-
1343
- private
1344
-
1345
- def user_params
1346
- params.require(:user).permit(:name, :email, :password)
1347
- end
1348
- end
1349
- ```
1350
-
1351
- ## Troubleshooting
1352
-
1353
- ### Common Errors
1354
-
1355
- #### "Invalid property name"
1356
- Property names must start with a letter or underscore and can only contain letters, numbers, and underscores:
1357
-
1358
- ```ruby
1359
- # Invalid
1360
- property "1name", String # Starts with a number
1361
- property "name!", String # Contains a special character
1362
-
1363
- # Valid
1364
- property :name, String
1365
- property :user_name, String
1366
- ```
1367
-
1368
- #### "Property type is missing"
1369
- You must specify a type for each property:
1370
-
1371
- ```ruby
1372
- # Invalid
1373
- property :name
1374
-
1375
- # Valid
1376
- property :name, String
1377
- ```
1378
-
1379
- #### "Unknown option"
1380
- You specified an option that is not valid for the property type:
1381
-
1382
- ```ruby
1383
- # Invalid (min_length is for strings, not integers)
1384
- property :age, Integer, min_length: 2
1385
-
1386
- # Valid
1387
- property :age, Integer, minimum: 18
1388
- ```
1389
-
1390
- ### Schema Validation Issues
1391
- If you're having issues with validation:
1392
-
1393
- 1. Make sure you've defined ActiveModel validations for your model
1394
- 2. Check for mismatches between schema constraints and validations
1395
- 3. Verify that required properties are present
1396
-
1397
- ### Type Errors
1398
- Type errors usually occur when there's a mismatch between a property type and its constraints:
1399
-
1400
- ```ruby
1401
- # Error: enum values must be strings for a string property
1402
- property :status, String, enum: [1, 2, 3]
1403
-
1404
- # Correct
1405
- property :status, String, enum: ["active", "inactive", "pending"]
1406
- ```
1407
-
1408
- ### Best Practices
1409
-
1410
- 1. **Define clear property names and descriptions** for better documentation
1411
- 2. **Use appropriate types** for each property with proper constraints
1412
- 3. **Leverage automatic validations** by defining schema constraints instead of manual validations
1413
- 4. **Keep schemas focused and modular** - extract nested objects to separate classes
1414
- 5. **Reuse models when appropriate** instead of duplicating schema definitions
1415
- 6. **Use explicit types** instead of relying on inference
1416
- 7. **Test your schemas with sample data** to ensure validations work as expected
1417
- 8. **Configure auto-validations globally** to maintain consistency across your application
1418
- 9. **Use nullable_optional_property** for fields that can be omitted or null
1419
- 10. **Document breaking changes** when updating schema definitions
1420
-
1421
- ## Nullable vs Optional Properties in EasyTalk
1422
-
1423
- One of the most important distinctions when defining schemas is understanding the difference between **nullable** properties and **optional** properties. This guide explains these concepts and how to use them effectively in EasyTalk.
1424
-
1425
- ### Key Concepts
1426
-
1427
- | Concept | Description | JSON Schema Effect | EasyTalk Syntax |
1428
- |---------|-------------|-------------------|-----------------|
1429
- | **Nullable** | Property can have a `null` value | Adds `"null"` to the type array | `T.nilable(Type)` |
1430
- | **Optional** | Property doesn't have to exist | Omits property from `"required"` array | `optional: true` constraint |
1431
-
1432
- ### Nullable Properties
1433
-
1434
- A **nullable** property can contain a `null` value, but the property itself must still be present in the object:
1435
-
1436
- ```ruby
1437
- property :age, T.nilable(Integer)
1438
- ```
1439
-
1440
- This produces the following JSON Schema:
1441
-
1442
- ```json
1443
- {
1444
- "properties": {
1445
- "age": { "type": ["integer", "null"] }
1446
- },
1447
- "required": ["age"]
1448
- }
1449
- ```
1450
-
1451
- In this case, the following data would be valid:
1452
- - `{ "age": 25 }`
1453
- - `{ "age": null }`
1454
-
1455
- But this would be invalid:
1456
- - `{ }` (missing the age property entirely)
1457
-
1458
- ### Optional Properties
1459
-
1460
- An **optional** property doesn't have to be present in the object at all:
1461
-
1462
- ```ruby
1463
- property :nickname, String, optional: true
1464
- ```
1465
-
1466
- This produces:
1467
-
1468
- ```json
1469
- {
1470
- "properties": {
1471
- "nickname": { "type": "string" }
1472
- }
1473
- // Note: "nickname" is not in the "required" array
1474
- }
1475
- ```
1476
-
1477
- In this case, the following data would be valid:
1478
- - `{ "nickname": "Joe" }`
1479
- - `{ }` (omitting nickname entirely)
1480
-
1481
- But this would be invalid:
1482
- - `{ "nickname": null }` (null is not allowed because the property isn't nullable)
1483
-
1484
- ### Nullable AND Optional Properties
1485
-
1486
- For properties that should be both nullable and optional (can be omitted or null), you need to combine both approaches:
1487
-
1488
- ```ruby
1489
- property :bio, T.nilable(String), optional: true
1490
- ```
1491
-
1492
- This produces:
1493
-
1494
- ```json
1495
- {
1496
- "properties": {
1497
- "bio": { "type": ["string", "null"] }
1498
- }
1499
- // Note: "bio" is not in the "required" array
1500
- }
1501
- ```
1502
-
1503
- For convenience, EasyTalk also provides a helper method:
1504
-
1505
- ```ruby
1506
- nullable_optional_property :bio, String
1507
- ```
1508
-
1509
- Which is equivalent to the above.
1510
-
1511
- ### Configuration Options
1512
-
1513
- By default, nullable properties are still required. You can change this global behavior:
1514
-
1515
- ```ruby
1516
- EasyTalk.configure do |config|
1517
- config.nilable_is_optional = true # Makes all T.nilable properties also optional
1518
- end
1519
- ```
1520
-
1521
- With this configuration, any property defined with `T.nilable(Type)` will be treated as both nullable and optional.
1522
-
1523
- ### Practical Examples
1524
-
1525
- #### User Profile Schema
1526
-
1527
- ```ruby
1528
- class UserProfile
1529
- include EasyTalk::Model
1530
-
1531
- define_schema do
1532
- # Required properties (must exist, cannot be null)
1533
- property :id, String
1534
- property :name, String
1535
-
1536
- # Required but nullable (must exist, can be null)
1537
- property :age, T.nilable(Integer)
1538
-
1539
- # Optional but not nullable (can be omitted, cannot be null if present)
1540
- property :email, String, optional: true
1541
-
1542
- # Optional and nullable (can be omitted, can be null if present)
1543
- nullable_optional_property :bio, String
1544
- end
1545
- end
1546
- ```
1547
-
1548
- This creates clear expectations for data validation:
1549
- - `id` and `name` must be present and cannot be null
1550
- - `age` must be present but can be null
1551
- - `email` doesn't have to be present, but if it is, it cannot be null
1552
- - `bio` doesn't have to be present, and if it is, it can be null
1553
-
1554
- ### Common Gotchas
1555
-
1556
- #### Misconception: Nullable Implies Optional
1557
-
1558
- A common mistake is assuming that `T.nilable(Type)` makes a property optional. By default, it only allows the property to have a null value - the property itself is still required to exist in the object.
1559
-
1560
- #### Misconception: Optional Properties Accept Null
1561
-
1562
- An optional property (defined with `optional: true`) can be omitted entirely, but if it is present, it must conform to its type constraint. If you want to allow null values, you must also make it nullable with `T.nilable(Type)`.
1563
-
1564
- ### Migration from Earlier Versions
1565
-
1566
- If you're upgrading from EasyTalk version 1.0.1 or earlier, be aware that the handling of nullable vs optional properties has been improved for clarity.
1567
-
1568
- To maintain backward compatibility with your existing code, you can use:
1569
-
1570
- ```ruby
1571
- EasyTalk.configure do |config|
1572
- config.nilable_is_optional = true # Makes T.nilable properties behave as they did before
1573
- end
1574
- ```
1575
-
1576
- We recommend updating your schema definitions to explicitly declare which properties are optional using the `optional: true` constraint, as this makes your intent clearer.
1577
-
1578
- ### Best Practices
1579
-
1580
- 1. **Be explicit about intent**: Always clarify whether properties should be nullable, optional, or both
1581
- 2. **Use the helper method**: For properties that are both nullable and optional, use `nullable_optional_property`
1582
- 3. **Document expectations**: Use comments to clarify validation requirements for complex schemas
1583
- 4. **Consider validation implications**: Remember that ActiveModel validations operate independently of the schema definition
1584
-
1585
- ### JSON Schema Comparison
1586
-
1587
- | EasyTalk Definition | Required | Nullable | JSON Schema Equivalent |
1588
- |--------------------|----------|----------|------------------------|
1589
- | `property :p, String` | Yes | No | `{ "properties": { "p": { "type": "string" } }, "required": ["p"] }` |
1590
- | `property :p, T.nilable(String)` | Yes | Yes | `{ "properties": { "p": { "type": ["string", "null"] } }, "required": ["p"] }` |
1591
- | `property :p, String, optional: true` | No | No | `{ "properties": { "p": { "type": "string" } } }` |
1592
- | `nullable_optional_property :p, String` | No | Yes | `{ "properties": { "p": { "type": ["string", "null"] } } }` |
1593
-
1594
- ## Migration Guide from v1.x to v2.0
1595
-
1596
- ### Breaking Changes Summary
1597
-
1598
- 1. **Removed Block-Style Sub-Schemas**: Hash-based nested definitions are no longer supported
1599
- 2. **Enhanced Validation System**: Automatic validation generation is now enabled by default
1600
- 3. **Improved Model Initialization**: Better support for nested model instantiation
1601
-
1602
- ### Migration Steps
1603
-
1604
- #### 1. Replace Hash-based Nested Schemas
1605
-
1606
- ```ruby
1607
- # OLD (v1.x) - No longer works
1608
- class User
1609
- include EasyTalk::Model
1610
- define_schema do
1611
- property :address, Hash do
1612
- property :street, String
1613
- property :city, String
1614
- end
1615
- end
1616
- end
1617
-
1618
- # NEW (v2.x) - Extract to separate classes
1619
- class Address
1620
- include EasyTalk::Model
1621
- define_schema do
1622
- property :street, String
1623
- property :city, String
1624
- end
1625
- end
1626
-
1627
- class User
1628
- include EasyTalk::Model
1629
- define_schema do
1630
- property :address, Address
1631
- end
1632
- end
1633
- ```
1634
-
1635
- #### 2. Review Automatic Validations
1636
-
1637
- With `auto_validations: true` (default), you may need to remove redundant manual validations:
1638
-
1639
- ```ruby
1640
- # OLD (v1.x) - Manual validations required
1641
- class User
1642
- include EasyTalk::Model
1643
-
1644
- validates :name, presence: true, length: { minimum: 2 }
1645
- validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
1646
-
1647
- define_schema do
1648
- property :name, String
1649
- property :email, String
1650
- end
1651
- end
1652
-
1653
- # NEW (v2.x) - Automatic validations from constraints
1654
- class User
1655
- include EasyTalk::Model
1656
-
1657
- # Only add validations not covered by schema constraints
1658
- validates :email, uniqueness: true
1659
-
1660
- define_schema do
1661
- property :name, String, min_length: 2 # Auto-generates presence + length
1662
- property :email, String, format: "email" # Auto-generates presence + format
1663
- end
1664
- end
1665
- ```
1666
-
1667
- #### 3. Configuration Updates
1668
-
1669
- Review your configuration for new options:
1670
-
1671
- ```ruby
1672
- EasyTalk.configure do |config|
1673
- # New option in v2.0
1674
- config.auto_validations = true # Enable/disable automatic validation generation
1675
-
1676
- # Existing options (unchanged)
1677
- config.nilable_is_optional = false
1678
- config.default_additional_properties = false
1679
- # ... other existing config
1680
- end
1681
- ```
1682
-
1683
- ### Compatibility Notes
1684
-
1685
- - **Ruby Version**: Still requires Ruby 3.2+
1686
- - **Dependencies**: Core dependencies remain the same
1687
- - **JSON Schema Output**: No changes to generated schemas
1688
- - **ActiveModel Integration**: Fully backward compatible
1689
-
1690
- ## Development and Contributing
1691
-
1692
- ### Setting Up the Development Environment
1693
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that lets you experiment.
1694
-
1695
- To install this gem onto your local machine, run:
1696
-
1697
- ```bash
1698
- bundle exec rake install
1699
- ```
1700
-
1701
- ### Running Tests
1702
- Run the test suite with:
1703
-
1704
- ```bash
1705
- bundle exec rake spec
1706
- ```
1707
-
1708
- ### Code Quality
1709
- Run the linter:
1710
-
1711
- ```bash
1712
- bundle exec rubocop
1713
- ```
1714
-
1715
- ### Contributing Guidelines
1716
- Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
1717
-
1718
- ## JSON Schema Version (`$schema` Keyword)
1719
-
1720
- The `$schema` keyword declares which JSON Schema dialect (draft version) a schema conforms to. EasyTalk supports configuring this at both the global and per-model level.
1721
-
1722
- ### Why Use `$schema`?
1723
-
1724
- The `$schema` keyword:
1725
- - Declares the JSON Schema version your schema is written against
1726
- - Helps validators understand which specification to use
1727
- - Enables tooling to provide appropriate validation and autocomplete
1728
- - Documents the schema dialect for consumers of your API
1729
-
1730
- ### Supported Draft Versions
1731
-
1732
- EasyTalk supports the following JSON Schema draft versions:
1733
-
1734
- | Symbol | JSON Schema Version | URI |
1735
- |--------|---------------------|-----|
1736
- | `:draft202012` | Draft 2020-12 (latest) | `https://json-schema.org/draft/2020-12/schema` |
1737
- | `:draft201909` | Draft 2019-09 | `https://json-schema.org/draft/2019-09/schema` |
1738
- | `:draft7` | Draft-07 | `http://json-schema.org/draft-07/schema#` |
1739
- | `:draft6` | Draft-06 | `http://json-schema.org/draft-06/schema#` |
1740
- | `:draft4` | Draft-04 | `http://json-schema.org/draft-04/schema#` |
1741
- | `:none` | No `$schema` output (default) | N/A |
1742
-
1743
- ### Global Configuration
1744
-
1745
- Configure the schema version globally to apply to all models:
1746
-
1747
- ```ruby
1748
- EasyTalk.configure do |config|
1749
- config.schema_version = :draft202012 # Use JSON Schema Draft 2020-12
1750
- end
1751
- ```
1752
-
1753
- With this configuration, all models will include `$schema` in their output:
1754
-
1755
- ```ruby
1756
- class User
1757
- include EasyTalk::Model
1758
-
1759
- define_schema do
1760
- property :name, String
658
+ property :street, String
659
+ property :city, String
1761
660
  end
1762
661
  end
1763
662
 
1764
- User.json_schema
1765
- # => {
1766
- # "$schema" => "https://json-schema.org/draft/2020-12/schema",
1767
- # "type" => "object",
1768
- # "properties" => { "name" => { "type" => "string" } },
1769
- # "required" => ["name"],
1770
- # "additionalProperties" => false
1771
- # }
1772
- ```
1773
-
1774
- ### Per-Model Configuration
1775
-
1776
- Override the global setting for individual models using the `schema_version` keyword in the schema definition:
1777
-
1778
- ```ruby
1779
- class LegacyModel
663
+ class Customer
1780
664
  include EasyTalk::Model
1781
665
 
1782
666
  define_schema do
1783
- schema_version :draft7 # Use Draft-07 for this specific model
1784
667
  property :name, String
668
+ property :address, Address
1785
669
  end
1786
670
  end
1787
671
 
1788
- LegacyModel.json_schema
1789
- # => {
1790
- # "$schema" => "http://json-schema.org/draft-07/schema#",
1791
- # "type" => "object",
1792
- # ...
1793
- # }
1794
- ```
1795
-
1796
- ### Disabling `$schema` for Specific Models
1797
-
1798
- If you have a global schema version configured but want to exclude `$schema` from a specific model, use `:none`:
1799
-
1800
- ```ruby
1801
- EasyTalk.configure do |config|
1802
- config.schema_version = :draft202012 # Global default
1803
- end
1804
-
1805
- class InternalModel
1806
- include EasyTalk::Model
1807
-
1808
- define_schema do
1809
- schema_version :none # No $schema for this model
1810
- property :data, String
1811
- end
1812
- end
1813
-
1814
- InternalModel.json_schema
1815
- # => {
1816
- # "type" => "object",
1817
- # "properties" => { "data" => { "type" => "string" } },
1818
- # ...
1819
- # }
1820
- # Note: No "$schema" key present
672
+ Customer.json_schema
1821
673
  ```
1822
674
 
1823
- ### Custom Schema URIs
1824
-
1825
- You can also specify a custom URI if you're using a custom meta-schema or a different schema registry:
1826
-
1827
- ```ruby
1828
- class CustomModel
1829
- include EasyTalk::Model
675
+ </td>
676
+ <td>
1830
677
 
1831
- define_schema do
1832
- schema_version 'https://my-company.com/schemas/v1/meta-schema.json'
1833
- property :id, String
1834
- end
1835
- end
678
+ ```json
679
+ {
680
+ "properties": {
681
+ "address": {
682
+ "$ref": "https://example.com/schemas/address"
683
+ }
684
+ },
685
+ "$defs": {
686
+ "Address": {
687
+ "$id": "https://example.com/schemas/address",
688
+ "properties": {
689
+ "street": { "type": "string" },
690
+ "city": { "type": "string" }
691
+ }
692
+ }
693
+ }
694
+ }
1836
695
  ```
1837
696
 
1838
- ### Nested Models
697
+ </td>
698
+ </tr>
699
+ </table>
1839
700
 
1840
- The `$schema` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$schema`:
701
+ **Explicit schema IDs:**
1841
702
 
1842
703
  ```ruby
1843
- EasyTalk.configure do |config|
1844
- config.schema_version = :draft202012
1845
- end
1846
-
1847
704
  class Address
1848
705
  include EasyTalk::Model
1849
- define_schema do
1850
- property :city, String
1851
- end
1852
- end
1853
706
 
1854
- class User
1855
- include EasyTalk::Model
1856
707
  define_schema do
1857
- property :name, String
1858
- property :address, Address
708
+ schema_id 'https://example.com/schemas/address'
709
+ property :street, String
1859
710
  end
1860
711
  end
1861
-
1862
- User.json_schema
1863
- # => {
1864
- # "$schema" => "https://json-schema.org/draft/2020-12/schema", # Only at root
1865
- # "type" => "object",
1866
- # "properties" => {
1867
- # "name" => { "type" => "string" },
1868
- # "address" => {
1869
- # "type" => "object", # No $schema here
1870
- # "properties" => { "city" => { "type" => "string" } },
1871
- # ...
1872
- # }
1873
- # },
1874
- # ...
1875
- # }
1876
- ```
1877
-
1878
- ### Default Behavior
1879
-
1880
- By default, `schema_version` is set to `:none`, meaning no `$schema` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
1881
-
1882
- ### Best Practices
1883
-
1884
- 1. **Choose a version appropriate for your validators**: If you're using a specific JSON Schema validator, check which drafts it supports.
1885
-
1886
- 2. **Use Draft 2020-12 for new projects**: It's the latest stable version with the most features.
1887
-
1888
- 3. **Be consistent**: Use global configuration for consistency across your application, and only override per-model when necessary.
1889
-
1890
- 4. **Consider your consumers**: If your schemas are consumed by external systems, ensure they support the draft version you're using.
1891
-
1892
- ## Schema Identifier (`$id` Keyword)
1893
-
1894
- The `$id` keyword provides a unique identifier for your JSON Schema document. EasyTalk supports configuring this at both the global and per-model level.
1895
-
1896
- ### Why Use `$id`?
1897
-
1898
- The `$id` keyword:
1899
- - Establishes a unique URI identifier for the schema
1900
- - Enables referencing schemas from other documents via `$ref`
1901
- - Provides a base URI for resolving relative references within the schema
1902
- - Documents the canonical location of the schema
1903
-
1904
- ### Global Configuration
1905
-
1906
- Configure the schema ID globally to apply to all models:
1907
-
1908
- ```ruby
1909
- EasyTalk.configure do |config|
1910
- config.schema_id = 'https://example.com/schemas/base.json'
1911
- end
1912
712
  ```
1913
713
 
1914
- With this configuration, all models will include `$id` in their output:
714
+ **Per-property ref control:**
1915
715
 
1916
716
  ```ruby
1917
- class User
717
+ class Customer
1918
718
  include EasyTalk::Model
1919
719
 
1920
720
  define_schema do
1921
- property :name, String
721
+ property :address, Address, ref: false # Inline instead of ref
722
+ property :billing, Address # Uses ref (global setting)
1922
723
  end
1923
724
  end
1924
-
1925
- User.json_schema
1926
- # => {
1927
- # "$id" => "https://example.com/schemas/base.json",
1928
- # "type" => "object",
1929
- # "properties" => { "name" => { "type" => "string" } },
1930
- # "required" => ["name"],
1931
- # "additionalProperties" => false
1932
- # }
1933
725
  ```
1934
726
 
1935
- ### Per-Model Configuration
727
+ ### Additional properties with types
1936
728
 
1937
- Override the global setting for individual models using the `schema_id` keyword in the schema definition:
729
+ Beyond boolean values, `additional_properties` now supports type constraints for dynamic properties:
1938
730
 
1939
731
  ```ruby
1940
- class User
732
+ class Config
1941
733
  include EasyTalk::Model
1942
734
 
1943
735
  define_schema do
1944
- schema_id 'https://example.com/schemas/user.schema.json'
1945
736
  property :name, String
1946
- property :email, String
737
+
738
+ # Allow any string-typed additional properties
739
+ additional_properties String
1947
740
  end
1948
741
  end
1949
742
 
1950
- User.json_schema
1951
- # => {
1952
- # "$id" => "https://example.com/schemas/user.schema.json",
1953
- # "type" => "object",
1954
- # ...
1955
- # }
743
+ config = Config.new(name: 'app')
744
+ config.label = 'Production' # Dynamic property
745
+ config.as_json
746
+ # => { 'name' => 'app', 'label' => 'Production' }
1956
747
  ```
1957
748
 
1958
- ### Disabling `$id` for Specific Models
749
+ **With constraints:**
1959
750
 
1960
- If you have a global schema ID configured but want to exclude `$id` from a specific model, use `:none`:
751
+ <table>
752
+ <tr>
753
+ <th>EasyTalk Model</th>
754
+ <th>Generated JSON Schema</th>
755
+ </tr>
756
+ <tr>
757
+ <td>
1961
758
 
1962
759
  ```ruby
1963
- EasyTalk.configure do |config|
1964
- config.schema_id = 'https://example.com/schemas/default.json'
1965
- end
1966
-
1967
- class InternalModel
760
+ class StrictConfig
1968
761
  include EasyTalk::Model
1969
762
 
1970
763
  define_schema do
1971
- schema_id :none # No $id for this model
1972
- property :data, String
764
+ property :id, Integer
765
+ # Integer values between 0 and 100 only
766
+ additional_properties Integer,
767
+ minimum: 0, maximum: 100
1973
768
  end
1974
769
  end
1975
770
 
1976
- InternalModel.json_schema
1977
- # => {
1978
- # "type" => "object",
1979
- # "properties" => { "data" => { "type" => "string" } },
1980
- # ...
1981
- # }
1982
- # Note: No "$id" key present
771
+ StrictConfig.json_schema
1983
772
  ```
1984
773
 
1985
- ### Combining `$schema` and `$id`
1986
-
1987
- When both `$schema` and `$id` are configured, they appear in the standard order (`$schema` first, then `$id`):
1988
-
1989
- ```ruby
1990
- class Product
1991
- include EasyTalk::Model
1992
-
1993
- define_schema do
1994
- schema_version :draft202012
1995
- schema_id 'https://example.com/schemas/product.schema.json'
1996
- property :name, String
1997
- property :price, Float
1998
- end
1999
- end
774
+ </td>
775
+ <td>
2000
776
 
2001
- Product.json_schema
2002
- # => {
2003
- # "$schema" => "https://json-schema.org/draft/2020-12/schema",
2004
- # "$id" => "https://example.com/schemas/product.schema.json",
2005
- # "type" => "object",
2006
- # ...
2007
- # }
777
+ ```json
778
+ {
779
+ "properties": {
780
+ "id": { "type": "integer" }
781
+ },
782
+ "additionalProperties": {
783
+ "type": "integer",
784
+ "minimum": 0,
785
+ "maximum": 100
786
+ }
787
+ }
2008
788
  ```
2009
789
 
2010
- ### Nested Models
790
+ </td>
791
+ </tr>
792
+ </table>
2011
793
 
2012
- The `$id` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$id`:
794
+ **Nested models as additional properties:**
2013
795
 
2014
796
  ```ruby
2015
- EasyTalk.configure do |config|
2016
- config.schema_id = 'https://example.com/schemas/user.json'
2017
- end
2018
-
2019
- class Address
797
+ class Person
2020
798
  include EasyTalk::Model
2021
- define_schema do
2022
- property :city, String
2023
- end
2024
- end
2025
799
 
2026
- class User
2027
- include EasyTalk::Model
2028
800
  define_schema do
2029
801
  property :name, String
2030
- property :address, Address
802
+ additional_properties Address # All additional properties must be Address objects
2031
803
  end
2032
804
  end
2033
-
2034
- User.json_schema
2035
- # => {
2036
- # "$id" => "https://example.com/schemas/user.json", # Only at root
2037
- # "type" => "object",
2038
- # "properties" => {
2039
- # "name" => { "type" => "string" },
2040
- # "address" => {
2041
- # "type" => "object", # No $id here
2042
- # "properties" => { "city" => { "type" => "string" } },
2043
- # ...
2044
- # }
2045
- # },
2046
- # ...
2047
- # }
2048
- ```
2049
-
2050
- ### URI Formats
2051
-
2052
- The `$id` accepts various URI formats:
2053
-
2054
- ```ruby
2055
- # Absolute URI (recommended for published schemas)
2056
- schema_id 'https://example.com/schemas/user.schema.json'
2057
-
2058
- # Relative URI
2059
- schema_id 'user.schema.json'
2060
-
2061
- # URN format
2062
- schema_id 'urn:example:user-schema'
2063
805
  ```
2064
806
 
2065
- ### Default Behavior
2066
-
2067
- By default, `schema_id` is set to `nil`, meaning no `$id` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
2068
-
2069
- ### Best Practices
2070
-
2071
- 1. **Use absolute URIs for published schemas**: This ensures global uniqueness and enables external references.
2072
-
2073
- 2. **Follow a consistent naming convention**: For example, `https://yourdomain.com/schemas/{model-name}.schema.json`.
2074
-
2075
- 3. **Keep IDs stable**: Once published, avoid changing schema IDs as external systems may reference them.
2076
-
2077
- 4. **Combine with `$schema`**: When publishing schemas, include both `$schema` (for validation) and `$id` (for identification).
807
+ ### Object-level constraints
2078
808
 
2079
- ## Schema References (`$ref` and `$defs`)
2080
-
2081
- The `$ref` keyword allows you to reference reusable schema definitions, reducing duplication when the same model is used in multiple places. EasyTalk supports automatic `$ref` generation for nested models.
2082
-
2083
- ### Why Use `$ref`?
2084
-
2085
- The `$ref` keyword:
2086
- - Reduces schema duplication when the same model appears multiple times
2087
- - Produces cleaner, more organized schemas
2088
- - Improves schema readability and maintainability
2089
- - Aligns with JSON Schema best practices for reusable definitions
2090
-
2091
- ### Default Behavior (Inline Schemas)
2092
-
2093
- By default, EasyTalk inlines nested model schemas directly:
809
+ Apply schema-wide constraints to limit or validate object structure:
2094
810
 
2095
811
  ```ruby
2096
- class Address
812
+ class StrictObject
2097
813
  include EasyTalk::Model
2098
- define_schema do
2099
- property :street, String
2100
- property :city, String
2101
- end
2102
- end
2103
814
 
2104
- class Person
2105
- include EasyTalk::Model
2106
815
  define_schema do
2107
- property :name, String
2108
- property :address, Address
2109
- end
2110
- end
2111
-
2112
- Person.json_schema
2113
- # => {
2114
- # "type" => "object",
2115
- # "properties" => {
2116
- # "name" => { "type" => "string" },
2117
- # "address" => {
2118
- # "type" => "object",
2119
- # "properties" => {
2120
- # "street" => { "type" => "string" },
2121
- # "city" => { "type" => "string" }
2122
- # },
2123
- # ...
2124
- # }
2125
- # },
2126
- # ...
2127
- # }
2128
- ```
2129
-
2130
- ### Enabling `$ref` References
816
+ property :required1, String
817
+ property :required2, String
818
+ property :optional1, String, optional: true
819
+ property :optional2, String, optional: true
2131
820
 
2132
- #### Global Configuration
2133
-
2134
- Enable `$ref` generation for all nested models:
2135
-
2136
- ```ruby
2137
- EasyTalk.configure do |config|
2138
- config.use_refs = true
821
+ # Require at least 2 properties
822
+ min_properties 2
823
+ # Allow at most 3 properties
824
+ max_properties 3
825
+ end
2139
826
  end
2140
- ```
2141
-
2142
- With this configuration, nested models are referenced via `$ref` and their definitions are placed in `$defs`:
2143
827
 
2144
- ```ruby
2145
- Person.json_schema
2146
- # => {
2147
- # "type" => "object",
2148
- # "properties" => {
2149
- # "name" => { "type" => "string" },
2150
- # "address" => { "$ref" => "#/$defs/Address" }
2151
- # },
2152
- # "$defs" => {
2153
- # "Address" => {
2154
- # "type" => "object",
2155
- # "properties" => {
2156
- # "street" => { "type" => "string" },
2157
- # "city" => { "type" => "string" }
2158
- # },
2159
- # ...
2160
- # }
2161
- # },
2162
- # ...
2163
- # }
828
+ obj = StrictObject.new(required1: 'a')
829
+ obj.valid? # => false (only 1 property, needs at least 2)
2164
830
  ```
2165
831
 
2166
- #### Per-Property Configuration
2167
-
2168
- You can also enable `$ref` for specific properties using the `ref: true` constraint:
832
+ **Pattern properties:**
2169
833
 
2170
834
  ```ruby
2171
- class Person
835
+ class DynamicConfig
2172
836
  include EasyTalk::Model
2173
- define_schema do
2174
- property :name, String
2175
- property :address, Address, ref: true # Use $ref for this property
2176
- end
2177
- end
2178
- ```
2179
-
2180
- Or disable `$ref` for specific properties when it's enabled globally:
2181
-
2182
- ```ruby
2183
- EasyTalk.configure do |config|
2184
- config.use_refs = true
2185
- end
2186
837
 
2187
- class Person
2188
- include EasyTalk::Model
2189
838
  define_schema do
2190
839
  property :name, String
2191
- property :address, Address, ref: false # Inline this property despite global setting
2192
- end
2193
- end
2194
- ```
2195
-
2196
- ### Arrays of Models
2197
840
 
2198
- When using `$ref` with arrays of models, the `$ref` applies to the array items:
2199
-
2200
- ```ruby
2201
- EasyTalk.configure do |config|
2202
- config.use_refs = true
2203
- end
2204
-
2205
- class Company
2206
- include EasyTalk::Model
2207
- define_schema do
2208
- property :name, String
2209
- property :addresses, T::Array[Address]
841
+ # Properties matching /^env_/ must be strings
842
+ pattern_properties(
843
+ '^env_' => { type: 'string' }
844
+ )
2210
845
  end
2211
846
  end
2212
-
2213
- Company.json_schema
2214
- # => {
2215
- # "type" => "object",
2216
- # "properties" => {
2217
- # "name" => { "type" => "string" },
2218
- # "addresses" => {
2219
- # "type" => "array",
2220
- # "items" => { "$ref" => "#/$defs/Address" }
2221
- # }
2222
- # },
2223
- # "$defs" => {
2224
- # "Address" => { ... }
2225
- # },
2226
- # ...
2227
- # }
2228
- ```
2229
-
2230
- You can also use the per-property `ref` constraint with arrays:
2231
-
2232
- ```ruby
2233
- property :addresses, T::Array[Address], ref: true
2234
847
  ```
2235
848
 
2236
- ### Nilable Models with `$ref`
2237
-
2238
- When using `$ref` with nilable model types, EasyTalk uses `anyOf` to combine the reference with the null type:
849
+ **Dependent required:**
2239
850
 
2240
851
  ```ruby
2241
- EasyTalk.configure do |config|
2242
- config.use_refs = true
2243
- end
2244
-
2245
- class Person
852
+ class ShippingInfo
2246
853
  include EasyTalk::Model
2247
- define_schema do
2248
- property :name, String
2249
- property :address, T.nilable(Address)
2250
- end
2251
- end
2252
-
2253
- Person.json_schema
2254
- # => {
2255
- # "type" => "object",
2256
- # "properties" => {
2257
- # "name" => { "type" => "string" },
2258
- # "address" => {
2259
- # "anyOf" => [
2260
- # { "$ref" => "#/$defs/Address" },
2261
- # { "type" => "null" }
2262
- # ]
2263
- # }
2264
- # },
2265
- # "$defs" => {
2266
- # "Address" => { ... }
2267
- # },
2268
- # ...
2269
- # }
2270
- ```
2271
-
2272
- ### Multiple References to the Same Model
2273
854
 
2274
- When the same model is used multiple times, it only appears once in `$defs`:
2275
-
2276
- ```ruby
2277
- class Person
2278
- include EasyTalk::Model
2279
855
  define_schema do
2280
856
  property :name, String
2281
- property :home_address, Address, ref: true
2282
- property :work_address, Address, ref: true
2283
- property :shipping_addresses, T::Array[Address], ref: true
2284
- end
2285
- end
2286
-
2287
- Person.json_schema
2288
- # => {
2289
- # "type" => "object",
2290
- # "properties" => {
2291
- # "name" => { "type" => "string" },
2292
- # "home_address" => { "$ref" => "#/$defs/Address" },
2293
- # "work_address" => { "$ref" => "#/$defs/Address" },
2294
- # "shipping_addresses" => {
2295
- # "type" => "array",
2296
- # "items" => { "$ref" => "#/$defs/Address" }
2297
- # }
2298
- # },
2299
- # "$defs" => {
2300
- # "Address" => { ... } # Only defined once
2301
- # },
2302
- # ...
2303
- # }
2304
- ```
857
+ property :credit_card, String, optional: true
858
+ property :billing_address, String, optional: true
2305
859
 
2306
- ### Combining `$ref` with Other Constraints
2307
-
2308
- You can add additional constraints alongside `$ref`:
2309
-
2310
- ```ruby
2311
- class Person
2312
- include EasyTalk::Model
2313
- define_schema do
2314
- property :address, Address, ref: true, description: "Primary address", title: "Main Address"
860
+ # If credit_card is present, billing_address is required
861
+ dependent_required(
862
+ 'credit_card' => ['billing_address']
863
+ )
2315
864
  end
2316
865
  end
2317
-
2318
- Person.json_schema["properties"]["address"]
2319
- # => {
2320
- # "$ref" => "#/$defs/Address",
2321
- # "description" => "Primary address",
2322
- # "title" => "Main Address"
2323
- # }
2324
866
  ```
2325
867
 
2326
- ### Interaction with `compose`
868
+ ### Custom type builders
2327
869
 
2328
- When using `compose` with `T::AllOf`, `T::AnyOf`, or `T::OneOf`, the composed models are also placed in `$defs`:
870
+ Register custom types with their own schema builders:
2329
871
 
2330
872
  ```ruby
2331
- class Employee
2332
- include EasyTalk::Model
2333
- define_schema do
2334
- compose T::AllOf[Person, EmployeeDetails]
2335
- property :badge_number, String
2336
- end
873
+ EasyTalk.configure do |config|
874
+ config.register_type(Money, MoneySchemaBuilder)
2337
875
  end
2338
- ```
2339
-
2340
- If you also have properties using `$ref`, both the composed models and property models will appear in `$defs`.
2341
-
2342
- ### Best Practices
2343
-
2344
- 1. **Use global configuration for consistency**: If you prefer `$ref` style, enable it globally rather than per-property.
2345
-
2346
- 2. **Consider schema consumers**: Some JSON Schema validators and tools work better with inlined schemas, while others prefer `$ref`. Choose based on your use case.
2347
-
2348
- 3. **Use `$ref` for frequently reused models**: If a model appears in many places, `$ref` reduces schema size and improves maintainability.
2349
-
2350
- 4. **Keep inline for simple, single-use models**: For models used only once, inlining may be more readable.
2351
876
 
2352
- ### Default Behavior
2353
-
2354
- By default, `use_refs` is set to `false`, meaning nested models are inlined. This maintains backward compatibility with previous versions of EasyTalk.
2355
-
2356
- ## JSON Schema Compatibility
2357
-
2358
- ### Supported Versions
2359
- EasyTalk supports generating schemas compatible with JSON Schema Draft-04 through Draft 2020-12. Use the `schema_version` configuration option to declare which version your schemas conform to (see [JSON Schema Version](#json-schema-version-schema-keyword) above).
2360
-
2361
- While EasyTalk allows you to specify any draft version via the `$schema` keyword, the generated schema structure is generally compatible across versions. Some newer draft features may require manual adjustment.
2362
-
2363
- ### Specification Compliance
2364
- To learn about current capabilities, see the [spec/easy_talk/examples](https://github.com/sergiobayona/easy_talk/tree/main/spec/easy_talk/examples) folder. The examples illustrate how EasyTalk generates JSON Schema in different scenarios.
2365
-
2366
- ### Known Limitations
2367
- - Limited support for custom formats
2368
- - Some draft-specific keywords may not be supported
2369
- - Complex composition scenarios may require manual adjustment
877
+ # Or directly:
878
+ EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
879
+ ```
2370
880
 
2371
- ## API Reference
881
+ See the [Custom Type Builders documentation](https://rubydoc.info/gems/easy_talk/EasyTalk/Builders/Registry) for details on creating builders.
2372
882
 
2373
- For complete API documentation, visit [RubyDoc.info](https://rubydoc.info/gems/easy_talk).
883
+ ---
2374
884
 
2375
- ### Core Modules
885
+ ## Known limitations
2376
886
 
2377
- | Module | Description | Source |
2378
- |--------|-------------|--------|
2379
- | [`EasyTalk::Model`](https://rubydoc.info/gems/easy_talk/EasyTalk/Model) | Main mixin providing schema definition, validation, and JSON Schema generation | [lib/easy_talk/model.rb](lib/easy_talk/model.rb) |
2380
- | [`EasyTalk::Schema`](https://rubydoc.info/gems/easy_talk/EasyTalk/Schema) | Schema-only mode without ActiveModel validations | [lib/easy_talk/schema.rb](lib/easy_talk/schema.rb) |
2381
- | [`EasyTalk::SchemaDefinition`](https://rubydoc.info/gems/easy_talk/EasyTalk/SchemaDefinition) | DSL for defining properties, titles, and descriptions | [lib/easy_talk/schema_definition.rb](lib/easy_talk/schema_definition.rb) |
2382
- | [`EasyTalk::Property`](https://rubydoc.info/gems/easy_talk/EasyTalk/Property) | Property definition and type dispatching | [lib/easy_talk/property.rb](lib/easy_talk/property.rb) |
887
+ EasyTalk aims to produce broadly compatible JSON Schema, but:
888
+ - Some draft-specific keywords/features may require manual schema tweaks
889
+ - Custom formats are limited (extend via custom builders when needed)
890
+ - Extremely complex composition can outgrow “auto validations and may need manual validations or external schema validators
2383
891
 
2384
- ### Builders
892
+ ---
2385
893
 
2386
- Type-specific builders that convert Ruby definitions to JSON Schema. See the [builders directory](lib/easy_talk/builders/) for all available builders.
894
+ ## Contributing
2387
895
 
2388
- | Builder | Description |
2389
- |---------|-------------|
2390
- | [`ObjectBuilder`](lib/easy_talk/builders/object_builder.rb) | Handles EasyTalk::Model classes |
2391
- | [`StringBuilder`](lib/easy_talk/builders/string_builder.rb) | String type with format, pattern, length constraints |
2392
- | [`IntegerBuilder`](lib/easy_talk/builders/integer_builder.rb) | Integer type with min/max constraints |
2393
- | [`NumberBuilder`](lib/easy_talk/builders/number_builder.rb) | Float/Number type with numeric constraints |
2394
- | [`BooleanBuilder`](lib/easy_talk/builders/boolean_builder.rb) | Boolean type (T::Boolean) |
2395
- | [`TypedArrayBuilder`](lib/easy_talk/builders/typed_array_builder.rb) | Typed arrays (T::Array[Type]) |
2396
- | [`CompositionBuilder`](lib/easy_talk/builders/composition_builder.rb) | Composition types (T::AnyOf, T::OneOf, T::AllOf) |
2397
- | [`UnionBuilder`](lib/easy_talk/builders/union_builder.rb) | Nilable types (T.nilable) |
2398
- | [`TemporalBuilder`](lib/easy_talk/builders/temporal_builder.rb) | Date and DateTime types |
2399
- | [`BaseBuilder`](lib/easy_talk/builders/base_builder.rb) | Base class for all builders |
2400
- | [`Registry`](lib/easy_talk/builders/registry.rb) | Type-to-builder registration |
896
+ - Run `bin/setup`
897
+ - Run specs: `bundle exec rake spec`
898
+ - Run lint: `bundle exec rubocop`
2401
899
 
2402
- ### Configuration & Utilities
900
+ Bug reports and PRs welcome.
2403
901
 
2404
- | Class/Module | Description | Source |
2405
- |--------------|-------------|--------|
2406
- | [`EasyTalk::Configuration`](https://rubydoc.info/gems/easy_talk/EasyTalk/Configuration) | Global configuration options | [lib/easy_talk/configuration.rb](lib/easy_talk/configuration.rb) |
2407
- | [`EasyTalk::ValidationBuilder`](https://rubydoc.info/gems/easy_talk/EasyTalk/ValidationBuilder) | Generates ActiveModel validations from constraints | [lib/easy_talk/validation_builder.rb](lib/easy_talk/validation_builder.rb) |
2408
- | [`EasyTalk::ErrorFormatter`](https://rubydoc.info/gems/easy_talk/EasyTalk/ErrorFormatter) | Formats validation errors (flat, JSON Pointer, RFC 7807, JSON:API) | [lib/easy_talk/error_formatter.rb](lib/easy_talk/error_formatter.rb) |
2409
- | [`EasyTalk::TypeIntrospection`](https://rubydoc.info/gems/easy_talk/EasyTalk/TypeIntrospection) | Type detection utilities | [lib/easy_talk/type_introspection.rb](lib/easy_talk/type_introspection.rb) |
2410
- | [`EasyTalk::Tools::FunctionBuilder`](https://rubydoc.info/gems/easy_talk/EasyTalk/Tools/FunctionBuilder) | LLM function specification generator | [lib/easy_talk/tools/function_builder.rb](lib/easy_talk/tools/function_builder.rb) |
902
+ ---
2411
903
 
2412
904
  ## License
2413
905
 
2414
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
906
+ MIT