easy_talk 3.2.0 → 3.3.0
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -43
- data/CHANGELOG.md +89 -0
- data/README.md +447 -2115
- data/docs/json_schema_compliance.md +140 -26
- data/docs/primitive-schema-rfc.md +894 -0
- data/lib/easy_talk/builders/base_builder.rb +2 -1
- data/lib/easy_talk/builders/boolean_builder.rb +2 -1
- data/lib/easy_talk/builders/collection_helpers.rb +4 -0
- data/lib/easy_talk/builders/composition_builder.rb +7 -2
- data/lib/easy_talk/builders/integer_builder.rb +2 -1
- data/lib/easy_talk/builders/null_builder.rb +4 -1
- data/lib/easy_talk/builders/number_builder.rb +4 -1
- data/lib/easy_talk/builders/object_builder.rb +64 -3
- data/lib/easy_talk/builders/registry.rb +15 -1
- data/lib/easy_talk/builders/string_builder.rb +3 -1
- data/lib/easy_talk/builders/temporal_builder.rb +7 -0
- data/lib/easy_talk/builders/tuple_builder.rb +89 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +4 -2
- data/lib/easy_talk/builders/union_builder.rb +5 -1
- data/lib/easy_talk/configuration.rb +17 -2
- data/lib/easy_talk/errors.rb +1 -0
- data/lib/easy_talk/errors_helper.rb +3 -0
- data/lib/easy_talk/json_schema_equality.rb +46 -0
- data/lib/easy_talk/keywords.rb +0 -1
- data/lib/easy_talk/model.rb +27 -1
- data/lib/easy_talk/model_helper.rb +4 -0
- data/lib/easy_talk/naming_strategies.rb +4 -0
- data/lib/easy_talk/property.rb +7 -0
- data/lib/easy_talk/ref_helper.rb +6 -0
- data/lib/easy_talk/schema.rb +1 -0
- data/lib/easy_talk/schema_definition.rb +52 -6
- data/lib/easy_talk/schema_methods.rb +36 -5
- data/lib/easy_talk/sorbet_extension.rb +1 -0
- data/lib/easy_talk/type_introspection.rb +45 -1
- data/lib/easy_talk/types/tuple.rb +77 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +350 -62
- data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
- data/lib/easy_talk/validation_adapters/base.rb +12 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +9 -0
- data/lib/easy_talk/validation_builder.rb +1 -0
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +1 -0
- metadata +13 -4
data/README.md
CHANGED
|
@@ -2,2413 +2,745 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/easy_talk)
|
|
4
4
|
[](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml)
|
|
5
|
+
[](https://codecov.io/gh/sergiobayona/easy_talk)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.ruby-lang.org)
|
|
8
|
+
[](https://rubygems.org/gems/easy_talk)
|
|
5
9
|
[](https://rubydoc.info/gems/easy_talk)
|
|
10
|
+
[](https://github.com/sergiobayona/easy_talk)
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
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)
|
|
162
|
-
|
|
163
|
-
## Introduction
|
|
164
|
-
|
|
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.
|
|
167
|
-
|
|
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.
|
|
178
|
-
|
|
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
|
|
185
|
-
|
|
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.
|
|
12
|
+
Ruby library for defining **structured data contracts** that generate **JSON Schema** *and* (optionally) **runtime validations** from the same definition.
|
|
188
13
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
### Requirements
|
|
192
|
-
- Ruby 3.2 or higher
|
|
193
|
-
|
|
194
|
-
### Version 2.0.0 Breaking Changes
|
|
195
|
-
|
|
196
|
-
⚠️ **IMPORTANT**: Version 2.0.0 includes breaking changes. Please review before upgrading:
|
|
197
|
-
|
|
198
|
-
- **Removed**: Block-style nested object definitions (using `Hash do ... end`)
|
|
199
|
-
- **Migration**: Use class references instead of inline Hash definitions
|
|
200
|
-
|
|
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
|
|
209
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
class User
|
|
220
|
-
include EasyTalk::Model
|
|
221
|
-
define_schema do
|
|
222
|
-
property :address, Address # Reference the class directly
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### Installation Steps
|
|
228
|
-
Add EasyTalk to your application's Gemfile:
|
|
229
|
-
|
|
230
|
-
```ruby
|
|
231
|
-
gem 'easy_talk'
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Or install it directly:
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
$ gem install easy_talk
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Verification
|
|
241
|
-
After installation, you can verify it's working by creating a simple model:
|
|
242
|
-
|
|
243
|
-
```ruby
|
|
244
|
-
require 'easy_talk'
|
|
245
|
-
|
|
246
|
-
class Test
|
|
247
|
-
include EasyTalk::Model
|
|
248
|
-
|
|
249
|
-
define_schema do
|
|
250
|
-
property :name, String
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
puts Test.json_schema
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## Quick Start
|
|
258
|
-
|
|
259
|
-
### Minimal Example
|
|
260
|
-
Here's a basic example to get you started with EasyTalk:
|
|
261
|
-
|
|
262
|
-
```ruby
|
|
263
|
-
class User
|
|
264
|
-
include EasyTalk::Model
|
|
265
|
-
|
|
266
|
-
define_schema do
|
|
267
|
-
title "User"
|
|
268
|
-
description "A user of the system"
|
|
269
|
-
property :name, String, description: "The user's name"
|
|
270
|
-
property :email, String, format: "email"
|
|
271
|
-
property :age, Integer, minimum: 18
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Generated JSON Schema
|
|
277
|
-
Calling `User.json_schema` will generate:
|
|
278
|
-
|
|
279
|
-
```ruby
|
|
280
|
-
{
|
|
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
|
-
}
|
|
297
|
-
},
|
|
298
|
-
"required" => ["name", "email", "age"]
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
### Basic Usage
|
|
303
|
-
Creating and validating an instance of your model:
|
|
304
|
-
|
|
305
|
-
```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"]
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
## Core Concepts
|
|
315
|
-
|
|
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.
|
|
318
|
-
|
|
319
|
-
```ruby
|
|
320
|
-
class MyModel
|
|
321
|
-
include EasyTalk::Model
|
|
322
|
-
|
|
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
|
-
```
|
|
331
|
-
|
|
332
|
-
### Property Types
|
|
333
|
-
|
|
334
|
-
#### Ruby Types
|
|
335
|
-
EasyTalk supports standard Ruby types directly:
|
|
336
|
-
|
|
337
|
-
- `String`: String values
|
|
338
|
-
- `Integer`: Integer values
|
|
339
|
-
- `Float`: Floating-point numbers
|
|
340
|
-
- `Date`: Date values
|
|
341
|
-
- `DateTime`: Date and time values
|
|
342
|
-
|
|
343
|
-
#### Sorbet-Style Types
|
|
344
|
-
For complex types, EasyTalk uses Sorbet-style type notation:
|
|
345
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
#### Custom Types
|
|
351
|
-
EasyTalk supports special composition types:
|
|
352
|
-
|
|
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
|
|
356
|
-
|
|
357
|
-
### Property Constraints
|
|
358
|
-
Property constraints depend on the type of property. Some common constraints include:
|
|
359
|
-
|
|
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
|
|
369
|
-
|
|
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`:
|
|
372
|
-
|
|
373
|
-
```ruby
|
|
374
|
-
define_schema do
|
|
375
|
-
property :name, String
|
|
376
|
-
property :middle_name, String, optional: true
|
|
377
|
-
end
|
|
378
|
-
```
|
|
379
|
-
|
|
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:
|
|
384
|
-
|
|
385
|
-
```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"] }
|
|
400
|
-
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
|
-
```
|
|
409
|
-
|
|
410
|
-
### Manual Validation Overrides
|
|
411
|
-
You can still add manual validations alongside automatic ones:
|
|
412
|
-
|
|
413
|
-
```ruby
|
|
414
|
-
class User
|
|
415
|
-
include EasyTalk::Model
|
|
416
|
-
|
|
417
|
-
# Custom validation in addition to automatic ones
|
|
418
|
-
validates :email, uniqueness: true
|
|
419
|
-
validate :complex_business_rule
|
|
420
|
-
|
|
421
|
-
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
|
|
431
|
-
end
|
|
432
|
-
end
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
## Defining Schemas
|
|
436
|
-
|
|
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
|
|
442
|
-
include EasyTalk::Model
|
|
443
|
-
|
|
444
|
-
define_schema do
|
|
445
|
-
title "Person"
|
|
446
|
-
property :name, String
|
|
447
|
-
property :age, Integer
|
|
448
|
-
end
|
|
449
|
-
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
|
-
```
|
|
14
|
+
Think “Pydantic-style ergonomics” for Ruby, with first-class JSON Schema output.
|
|
459
15
|
|
|
460
|
-
|
|
461
|
-
Arrays can be defined using the `T::Array` type:
|
|
16
|
+
---
|
|
462
17
|
|
|
463
|
-
|
|
464
|
-
property :tags, T::Array[String], min_items: 1, unique_items: true
|
|
465
|
-
property :scores, T::Array[Integer], description: "List of scores"
|
|
466
|
-
```
|
|
18
|
+
## Why EasyTalk?
|
|
467
19
|
|
|
468
|
-
You can
|
|
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.
|
|
469
21
|
|
|
470
|
-
|
|
471
|
-
property :addresses, T::Array[Address], description: "List of addresses"
|
|
472
|
-
```
|
|
22
|
+
EasyTalk makes the schema definition the single source of truth, so you can:
|
|
473
23
|
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
476
28
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
29
|
+
- **Stop arguing with JSON Schema’s verbosity**
|
|
30
|
+
Express constraints in Ruby where you already live:
|
|
31
|
+
```ruby
|
|
480
32
|
property :email, String, format: "email"
|
|
481
|
-
property :
|
|
482
|
-
property :
|
|
483
|
-
|
|
484
|
-
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
|
-
```
|
|
492
|
-
|
|
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 |
|
|
506
|
-
|
|
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:
|
|
509
|
-
|
|
510
|
-
```ruby
|
|
511
|
-
define_schema do
|
|
512
|
-
property :name, String
|
|
513
|
-
additional_properties true
|
|
514
|
-
end
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
With `additional_properties true`, you can add arbitrary properties to your model instances:
|
|
518
|
-
|
|
519
|
-
```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:
|
|
528
|
-
|
|
529
|
-
```ruby
|
|
530
|
-
EasyTalk.configure do |config|
|
|
531
|
-
config.property_naming_strategy = :snake_case # Options: :identity, :snake_case, :camel_case, :pascal_case
|
|
532
|
-
end
|
|
533
|
-
|
|
534
|
-
define_schema do
|
|
535
|
-
property_naming_strategy :camel_case # Overrides global setting for this schema
|
|
536
|
-
property :name, String
|
|
537
|
-
end
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
This affects how property names are represented in the generated JSON Schema.
|
|
541
|
-
Additionally, names can be overridden per property:
|
|
542
|
-
|
|
543
|
-
```ruby
|
|
544
|
-
property :first_name, String, as: "firstName" # Overrides global naming strategy
|
|
545
|
-
```
|
|
33
|
+
property :age, Integer, minimum: 18
|
|
34
|
+
property :tags, T::Array[String], min_items: 1
|
|
35
|
+
```
|
|
546
36
|
|
|
547
|
-
|
|
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
|
|
548
44
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
```ruby
|
|
553
|
-
class Payment
|
|
554
|
-
include EasyTalk::Model
|
|
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.
|
|
555
47
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
561
54
|
|
|
562
|
-
|
|
563
|
-
|
|
55
|
+
- **LLM tool/function schemas without a second schema layer**
|
|
56
|
+
Use the same contract to generate JSON Schema for function/tool calling.
|
|
564
57
|
|
|
565
|
-
|
|
566
|
-
class Contact
|
|
567
|
-
include EasyTalk::Model
|
|
58
|
+
EasyTalk is for teams who want their data contracts to be **correct, reusable, and boring** (the good kind of boring).
|
|
568
59
|
|
|
569
|
-
|
|
570
|
-
property :contact, T::OneOf[PhoneContact, EmailContact]
|
|
571
|
-
end
|
|
572
|
-
end
|
|
573
|
-
```
|
|
60
|
+
---
|
|
574
61
|
|
|
575
|
-
|
|
576
|
-
The `T::AllOf` type requires a property to match all of the specified schemas:
|
|
62
|
+
## Installation
|
|
577
63
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
include EasyTalk::Model
|
|
581
|
-
|
|
582
|
-
define_schema do
|
|
583
|
-
compose T::AllOf[VehicleIdentification, OwnerInfo, RegistrationDetails]
|
|
584
|
-
end
|
|
585
|
-
end
|
|
586
|
-
```
|
|
64
|
+
### Requirements
|
|
65
|
+
- Ruby **3.2+**
|
|
587
66
|
|
|
588
|
-
|
|
589
|
-
Composition types can be combined with arrays to define collections where each item must match one of several schemas:
|
|
67
|
+
Add to your Gemfile:
|
|
590
68
|
|
|
591
69
|
```ruby
|
|
592
|
-
|
|
593
|
-
include EasyTalk::Model
|
|
594
|
-
define_schema do
|
|
595
|
-
property :sku, String
|
|
596
|
-
property :weight, Float
|
|
597
|
-
end
|
|
598
|
-
end
|
|
599
|
-
|
|
600
|
-
class ProductB
|
|
601
|
-
include EasyTalk::Model
|
|
602
|
-
define_schema do
|
|
603
|
-
property :sku, String
|
|
604
|
-
property :digital_url, String
|
|
605
|
-
end
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
class Order
|
|
609
|
-
include EasyTalk::Model
|
|
610
|
-
|
|
611
|
-
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]]
|
|
615
|
-
end
|
|
616
|
-
end
|
|
70
|
+
gem "easy_talk"
|
|
617
71
|
```
|
|
618
72
|
|
|
619
|
-
|
|
73
|
+
Then:
|
|
620
74
|
|
|
621
|
-
```
|
|
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
|
-
}
|
|
75
|
+
```bash
|
|
76
|
+
bundle install
|
|
635
77
|
```
|
|
636
78
|
|
|
637
|
-
|
|
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
|
|
79
|
+
---
|
|
641
80
|
|
|
642
|
-
|
|
643
|
-
You can combine composition types to create complex schemas:
|
|
81
|
+
## Quick start
|
|
644
82
|
|
|
645
83
|
```ruby
|
|
646
|
-
|
|
647
|
-
include EasyTalk::Model
|
|
648
|
-
|
|
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
|
|
654
|
-
end
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
### Reusing Models
|
|
658
|
-
Models can reference other models to create hierarchical schemas:
|
|
659
|
-
|
|
660
|
-
```ruby
|
|
661
|
-
class Address
|
|
662
|
-
include EasyTalk::Model
|
|
663
|
-
|
|
664
|
-
define_schema do
|
|
665
|
-
property :street, String
|
|
666
|
-
property :city, String
|
|
667
|
-
property :state, String
|
|
668
|
-
property :zip, String
|
|
669
|
-
end
|
|
670
|
-
end
|
|
84
|
+
require "easy_talk"
|
|
671
85
|
|
|
672
86
|
class User
|
|
673
87
|
include EasyTalk::Model
|
|
674
|
-
|
|
675
|
-
define_schema do
|
|
676
|
-
property :name, String
|
|
677
|
-
property :address, Address
|
|
678
|
-
end
|
|
679
|
-
end
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
## ActiveModel Integration
|
|
683
|
-
|
|
684
|
-
### Enhanced Validation System
|
|
685
|
-
EasyTalk models include comprehensive ActiveModel validation support with automatic generation:
|
|
686
88
|
|
|
687
|
-
```ruby
|
|
688
|
-
class User
|
|
689
|
-
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
|
-
|
|
695
89
|
define_schema do
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
property :height, Float # Auto-generates presence validation (overridden above)
|
|
699
|
-
end
|
|
700
|
-
end
|
|
701
|
-
```
|
|
90
|
+
title "User"
|
|
91
|
+
description "A user of the system"
|
|
702
92
|
|
|
703
|
-
### Error Handling
|
|
704
|
-
You can access validation errors using the standard ActiveModel methods:
|
|
705
|
-
|
|
706
|
-
```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
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
### Standardized Error Formatting
|
|
715
|
-
|
|
716
|
-
EasyTalk provides multiple output formats for validation errors, making it easy to build consistent API responses.
|
|
717
|
-
|
|
718
|
-
#### Available Formats
|
|
719
|
-
|
|
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:
|
|
730
|
-
|
|
731
|
-
```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
|
-
user.validation_errors_flat
|
|
741
|
-
# => [{"field" => "name", "message" => "can't be blank", "code" => "blank"}]
|
|
742
|
-
|
|
743
|
-
# JSON Pointer format
|
|
744
|
-
user.validation_errors_json_pointer
|
|
745
|
-
# => [{"pointer" => "/properties/name", "message" => "can't be blank", "code" => "blank"}]
|
|
746
|
-
|
|
747
|
-
# RFC 7807 Problem Details
|
|
748
|
-
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
|
-
user.validation_errors_jsonapi
|
|
759
|
-
# => {
|
|
760
|
-
# "errors" => [
|
|
761
|
-
# {"status" => "422", "source" => {"pointer" => "/data/attributes/name"}, ...}
|
|
762
|
-
# ]
|
|
763
|
-
# }
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
#### Direct API Usage
|
|
767
|
-
|
|
768
|
-
You can also format errors directly using the `ErrorFormatter` module:
|
|
769
|
-
|
|
770
|
-
```ruby
|
|
771
|
-
EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: "User Validation Failed")
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
#### Configuration
|
|
775
|
-
|
|
776
|
-
Configure error formatting globally:
|
|
777
|
-
|
|
778
|
-
```ruby
|
|
779
|
-
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
|
|
783
|
-
end
|
|
784
|
-
```
|
|
785
|
-
|
|
786
|
-
### Model Attributes
|
|
787
|
-
EasyTalk models provide getters and setters for all defined properties:
|
|
788
|
-
|
|
789
|
-
```ruby
|
|
790
|
-
user = User.new
|
|
791
|
-
user.name = "John"
|
|
792
|
-
user.age = 30
|
|
793
|
-
puts user.name # => "John"
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
You can also initialize a model with a hash of attributes, including nested EasyTalk models:
|
|
797
|
-
|
|
798
|
-
```ruby
|
|
799
|
-
user = User.new(name: "John", age: 30, height: 5.9)
|
|
800
|
-
|
|
801
|
-
# NEW in v2.0.0: Automatic nested model instantiation
|
|
802
|
-
class Address
|
|
803
|
-
include EasyTalk::Model
|
|
804
|
-
define_schema do
|
|
805
|
-
property :street, String
|
|
806
|
-
property :city, String
|
|
807
|
-
end
|
|
808
|
-
end
|
|
809
|
-
|
|
810
|
-
class User
|
|
811
|
-
include EasyTalk::Model
|
|
812
|
-
define_schema do
|
|
813
|
-
property :name, String
|
|
814
|
-
property :address, Address
|
|
815
|
-
end
|
|
816
|
-
end
|
|
817
|
-
|
|
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"
|
|
825
|
-
```
|
|
826
|
-
|
|
827
|
-
## Advanced Features
|
|
828
|
-
|
|
829
|
-
### Schema-Only Mode (EasyTalk::Schema)
|
|
830
|
-
|
|
831
|
-
For scenarios where you need JSON Schema generation without ActiveModel validations, use `EasyTalk::Schema` instead of `EasyTalk::Model`. This is ideal for:
|
|
832
|
-
|
|
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
|
|
837
|
-
|
|
838
|
-
```ruby
|
|
839
|
-
class ApiContract
|
|
840
|
-
include EasyTalk::Schema # Not EasyTalk::Model
|
|
841
|
-
|
|
842
|
-
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
|
|
847
|
-
end
|
|
848
|
-
end
|
|
849
|
-
|
|
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'
|
|
857
|
-
|
|
858
|
-
# But no validation methods are available
|
|
859
|
-
contract.valid? # => NoMethodError
|
|
860
|
-
contract.errors # => NoMethodError
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
#### Key Differences from EasyTalk::Model
|
|
864
|
-
|
|
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:
|
|
882
|
-
|
|
883
|
-
```ruby
|
|
884
|
-
class Weather
|
|
885
|
-
include EasyTalk::Model
|
|
886
|
-
|
|
887
|
-
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"
|
|
892
|
-
end
|
|
893
|
-
end
|
|
894
|
-
|
|
895
|
-
function_spec = EasyTalk::Tools::FunctionBuilder.new(Weather)
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
This generates a function specification compatible with OpenAI's function calling API.
|
|
899
|
-
|
|
900
|
-
### Schema Transformation
|
|
901
|
-
You can transform EasyTalk schemas into various formats:
|
|
902
|
-
|
|
903
|
-
```ruby
|
|
904
|
-
# Get Ruby hash representation
|
|
905
|
-
schema_hash = User.schema
|
|
906
|
-
|
|
907
|
-
# Get JSON Schema representation
|
|
908
|
-
json_schema = User.json_schema
|
|
909
|
-
|
|
910
|
-
# Convert to JSON string
|
|
911
|
-
json_string = User.json_schema.to_json
|
|
912
|
-
```
|
|
913
|
-
|
|
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
|
-
```
|
|
924
|
-
|
|
925
|
-
### Custom Type Builders
|
|
926
|
-
|
|
927
|
-
EasyTalk provides a type registry that allows you to register custom types with their corresponding schema builders.
|
|
928
|
-
|
|
929
|
-
#### Registering Custom Types
|
|
930
|
-
|
|
931
|
-
Register types in your configuration:
|
|
932
|
-
|
|
933
|
-
```ruby
|
|
934
|
-
EasyTalk.configure do |config|
|
|
935
|
-
config.register_type(Money, MoneySchemaBuilder)
|
|
936
|
-
end
|
|
937
|
-
```
|
|
938
|
-
|
|
939
|
-
Or register directly with the registry:
|
|
940
|
-
|
|
941
|
-
```ruby
|
|
942
|
-
EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
#### Creating a Custom Builder
|
|
946
|
-
|
|
947
|
-
Custom builders extend `BaseBuilder` and implement the schema generation logic:
|
|
948
|
-
|
|
949
|
-
```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
|
|
968
|
-
end
|
|
969
|
-
|
|
970
|
-
# Register and use
|
|
971
|
-
EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
|
|
972
|
-
|
|
973
|
-
class Order
|
|
974
|
-
include EasyTalk::Model
|
|
975
|
-
|
|
976
|
-
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
93
|
property :id, String
|
|
1534
|
-
property :name, String
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
|
94
|
+
property :name, String, min_length: 2
|
|
95
|
+
property :email, String, format: "email"
|
|
96
|
+
property :age, Integer, minimum: 18
|
|
97
|
+
end
|
|
1680
98
|
end
|
|
1681
|
-
```
|
|
1682
99
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
- **ActiveModel Integration**: Fully backward compatible
|
|
100
|
+
User.json_schema # => Ruby Hash (JSON Schema)
|
|
101
|
+
user = User.new(name: "A") # invalid: min_length is 2
|
|
102
|
+
user.valid? # => false
|
|
103
|
+
user.errors # => ActiveModel::Errors
|
|
104
|
+
```
|
|
1689
105
|
|
|
1690
|
-
|
|
106
|
+
**Generated JSON Schema:**
|
|
1691
107
|
|
|
1692
|
-
|
|
1693
|
-
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"type": "object",
|
|
111
|
+
"title": "User",
|
|
112
|
+
"description": "A user of the system",
|
|
113
|
+
"properties": {
|
|
114
|
+
"id": { "type": "string" },
|
|
115
|
+
"name": { "type": "string", "minLength": 2 },
|
|
116
|
+
"email": { "type": "string", "format": "email" },
|
|
117
|
+
"age": { "type": "integer", "minimum": 18 }
|
|
118
|
+
},
|
|
119
|
+
"required": ["id", "name", "email", "age"]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
1694
122
|
|
|
1695
|
-
|
|
123
|
+
---
|
|
1696
124
|
|
|
1697
|
-
|
|
1698
|
-
bundle exec rake install
|
|
1699
|
-
```
|
|
125
|
+
## Property constraints
|
|
1700
126
|
|
|
1701
|
-
|
|
1702
|
-
|
|
127
|
+
| Constraint | Applies to | Example |
|
|
128
|
+
|------------|-----------|---------|
|
|
129
|
+
| `min_length` / `max_length` | String | `property :name, String, min_length: 2, max_length: 50` |
|
|
130
|
+
| `minimum` / `maximum` | Integer, Float | `property :age, Integer, minimum: 18, maximum: 120` |
|
|
131
|
+
| `format` | String | `property :email, String, format: "email"` |
|
|
132
|
+
| `pattern` | String | `property :zip, String, pattern: '^\d{5}$'` |
|
|
133
|
+
| `enum` | Any | `property :status, String, enum: ["active", "inactive"]` |
|
|
134
|
+
| `min_items` / `max_items` | Array, Tuple | `property :tags, T::Array[String], min_items: 1` |
|
|
135
|
+
| `unique_items` | Array, Tuple | `property :ids, T::Array[Integer], unique_items: true` |
|
|
136
|
+
| `additional_items` | Tuple | `property :coords, T::Tuple[Float, Float], additional_items: false` |
|
|
137
|
+
| `optional` | Any | `property :nickname, String, optional: true` |
|
|
138
|
+
| `default` | Any | `property :role, String, default: "user"` |
|
|
139
|
+
| `description` | Any | `property :name, String, description: "Full name"` |
|
|
140
|
+
| `title` | Any | `property :name, String, title: "User Name"` |
|
|
1703
141
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
142
|
+
**Object-level constraints** (applied in `define_schema` block):
|
|
143
|
+
- `min_properties` / `max_properties` - Minimum/maximum number of properties
|
|
144
|
+
- `pattern_properties` - Schema for properties matching regex patterns
|
|
145
|
+
- `dependent_required` - Conditional property requirements
|
|
1707
146
|
|
|
1708
|
-
|
|
1709
|
-
Run the linter:
|
|
147
|
+
When `auto_validations` is enabled (default), these constraints automatically generate corresponding ActiveModel validations.
|
|
1710
148
|
|
|
1711
|
-
|
|
1712
|
-
bundle exec rubocop
|
|
1713
|
-
```
|
|
149
|
+
---
|
|
1714
150
|
|
|
1715
|
-
|
|
1716
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
|
|
151
|
+
## Core concepts
|
|
1717
152
|
|
|
1718
|
-
|
|
153
|
+
### Required vs optional vs nullable (don't get tricked)
|
|
1719
154
|
|
|
1720
|
-
|
|
155
|
+
JSON Schema distinguishes:
|
|
156
|
+
- **Optional**: property may be omitted (not in `required`)
|
|
157
|
+
- **Nullable**: property may be `null` (type includes `"null"`)
|
|
1721
158
|
|
|
1722
|
-
|
|
159
|
+
EasyTalk mirrors that precisely:
|
|
1723
160
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
- Enables tooling to provide appropriate validation and autocomplete
|
|
1728
|
-
- Documents the schema dialect for consumers of your API
|
|
161
|
+
```ruby
|
|
162
|
+
class Profile
|
|
163
|
+
include EasyTalk::Model
|
|
1729
164
|
|
|
1730
|
-
|
|
165
|
+
define_schema do
|
|
166
|
+
# required, not nullable
|
|
167
|
+
property :name, String
|
|
1731
168
|
|
|
1732
|
-
|
|
169
|
+
# required, nullable (must exist, may be null)
|
|
170
|
+
property :age, T.nilable(Integer)
|
|
1733
171
|
|
|
1734
|
-
|
|
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 |
|
|
172
|
+
# optional, not nullable (may be omitted, but cannot be null if present)
|
|
173
|
+
property :nickname, String, optional: true
|
|
1742
174
|
|
|
1743
|
-
|
|
175
|
+
# optional + nullable (may be omitted OR null)
|
|
176
|
+
property :bio, T.nilable(String), optional: true
|
|
177
|
+
# or, equivalently:
|
|
178
|
+
nullable_optional_property :website, String
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
1744
182
|
|
|
1745
|
-
|
|
183
|
+
By default, `T.nilable(Type)` makes a field **nullable but still required**.
|
|
184
|
+
If you want “nilable implies optional” behavior globally:
|
|
1746
185
|
|
|
1747
186
|
```ruby
|
|
1748
187
|
EasyTalk.configure do |config|
|
|
1749
|
-
config.
|
|
188
|
+
config.nilable_is_optional = true
|
|
1750
189
|
end
|
|
1751
190
|
```
|
|
1752
191
|
|
|
1753
|
-
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Nested models (and automatic instantiation)
|
|
195
|
+
|
|
196
|
+
Define nested objects as separate classes, then reference them:
|
|
1754
197
|
|
|
1755
198
|
```ruby
|
|
199
|
+
class Address
|
|
200
|
+
include EasyTalk::Model
|
|
201
|
+
|
|
202
|
+
define_schema do
|
|
203
|
+
property :street, String
|
|
204
|
+
property :city, String
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
1756
208
|
class User
|
|
1757
209
|
include EasyTalk::Model
|
|
1758
210
|
|
|
1759
211
|
define_schema do
|
|
1760
212
|
property :name, String
|
|
213
|
+
property :address, Address
|
|
1761
214
|
end
|
|
1762
215
|
end
|
|
1763
216
|
|
|
1764
|
-
User.
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
# "properties" => { "name" => { "type" => "string" } },
|
|
1769
|
-
# "required" => ["name"],
|
|
1770
|
-
# "additionalProperties" => false
|
|
1771
|
-
# }
|
|
1772
|
-
```
|
|
217
|
+
user = User.new(
|
|
218
|
+
name: "John",
|
|
219
|
+
address: { street: "123 Main St", city: "Boston" } # Hash becomes Address automatically
|
|
220
|
+
)
|
|
1773
221
|
|
|
1774
|
-
|
|
222
|
+
user.address.class # => Address
|
|
223
|
+
```
|
|
1775
224
|
|
|
1776
|
-
|
|
225
|
+
Nested models inside arrays work too:
|
|
1777
226
|
|
|
1778
227
|
```ruby
|
|
1779
|
-
class
|
|
228
|
+
class Order
|
|
1780
229
|
include EasyTalk::Model
|
|
1781
230
|
|
|
1782
231
|
define_schema do
|
|
1783
|
-
|
|
1784
|
-
property :name, String
|
|
232
|
+
property :line_items, T::Array[Address], min_items: 1
|
|
1785
233
|
end
|
|
1786
234
|
end
|
|
1787
|
-
|
|
1788
|
-
LegacyModel.json_schema
|
|
1789
|
-
# => {
|
|
1790
|
-
# "$schema" => "http://json-schema.org/draft-07/schema#",
|
|
1791
|
-
# "type" => "object",
|
|
1792
|
-
# ...
|
|
1793
|
-
# }
|
|
1794
235
|
```
|
|
1795
236
|
|
|
1796
|
-
|
|
237
|
+
---
|
|
1797
238
|
|
|
1798
|
-
|
|
239
|
+
### Tuple arrays (fixed-position types)
|
|
1799
240
|
|
|
1800
|
-
|
|
1801
|
-
EasyTalk.configure do |config|
|
|
1802
|
-
config.schema_version = :draft202012 # Global default
|
|
1803
|
-
end
|
|
241
|
+
Use `T::Tuple` for arrays where each position has a specific type (e.g., coordinates, CSV rows, database records):
|
|
1804
242
|
|
|
1805
|
-
|
|
243
|
+
```ruby
|
|
244
|
+
class GeoLocation
|
|
1806
245
|
include EasyTalk::Model
|
|
1807
246
|
|
|
1808
247
|
define_schema do
|
|
1809
|
-
|
|
1810
|
-
|
|
248
|
+
property :name, String
|
|
249
|
+
# Fixed: [latitude, longitude]
|
|
250
|
+
property :coordinates, T::Tuple[Float, Float]
|
|
1811
251
|
end
|
|
1812
252
|
end
|
|
1813
253
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
# ...
|
|
1819
|
-
# }
|
|
1820
|
-
# Note: No "$schema" key present
|
|
254
|
+
location = GeoLocation.new(
|
|
255
|
+
name: 'Office',
|
|
256
|
+
coordinates: [40.7128, -74.0060]
|
|
257
|
+
)
|
|
1821
258
|
```
|
|
1822
259
|
|
|
1823
|
-
|
|
260
|
+
**Generated JSON Schema:**
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"properties": {
|
|
265
|
+
"coordinates": {
|
|
266
|
+
"type": "array",
|
|
267
|
+
"items": [
|
|
268
|
+
{ "type": "number" },
|
|
269
|
+
{ "type": "number" }
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
1824
275
|
|
|
1825
|
-
|
|
276
|
+
**Mixed-type tuples:**
|
|
1826
277
|
|
|
1827
278
|
```ruby
|
|
1828
|
-
class
|
|
279
|
+
class DataRow
|
|
1829
280
|
include EasyTalk::Model
|
|
1830
281
|
|
|
1831
282
|
define_schema do
|
|
1832
|
-
|
|
1833
|
-
property :
|
|
283
|
+
# Fixed: [name, age, active]
|
|
284
|
+
property :row, T::Tuple[String, Integer, T::Boolean]
|
|
1834
285
|
end
|
|
1835
286
|
end
|
|
1836
287
|
```
|
|
1837
288
|
|
|
1838
|
-
|
|
1839
|
-
|
|
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`:
|
|
289
|
+
**Controlling extra items:**
|
|
1841
290
|
|
|
1842
291
|
```ruby
|
|
1843
|
-
|
|
1844
|
-
|
|
292
|
+
define_schema do
|
|
293
|
+
# Reject extra items (strict tuple)
|
|
294
|
+
property :rgb, T::Tuple[Integer, Integer, Integer], additional_items: false
|
|
295
|
+
|
|
296
|
+
# Allow extra items of specific type
|
|
297
|
+
property :header_values, T::Tuple[String], additional_items: Integer
|
|
298
|
+
|
|
299
|
+
# Allow any extra items (default)
|
|
300
|
+
property :flexible, T::Tuple[String, Integer]
|
|
1845
301
|
end
|
|
302
|
+
```
|
|
1846
303
|
|
|
1847
|
-
|
|
304
|
+
**Tuple validation:**
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
model = GeoLocation.new(coordinates: [40.7, "invalid"])
|
|
308
|
+
model.valid? # => false
|
|
309
|
+
model.errors[:coordinates]
|
|
310
|
+
# => ["item at index 1 must be a Float"]
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### Composition (AnyOf / OneOf / AllOf)
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
class ProductA
|
|
1848
319
|
include EasyTalk::Model
|
|
1849
320
|
define_schema do
|
|
1850
|
-
property :
|
|
321
|
+
property :sku, String
|
|
322
|
+
property :weight, Float
|
|
1851
323
|
end
|
|
1852
324
|
end
|
|
1853
325
|
|
|
1854
|
-
class
|
|
326
|
+
class ProductB
|
|
1855
327
|
include EasyTalk::Model
|
|
1856
328
|
define_schema do
|
|
1857
|
-
property :
|
|
1858
|
-
property :
|
|
329
|
+
property :sku, String
|
|
330
|
+
property :color, String
|
|
1859
331
|
end
|
|
1860
332
|
end
|
|
1861
333
|
|
|
1862
|
-
|
|
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.
|
|
334
|
+
class Cart
|
|
335
|
+
include EasyTalk::Model
|
|
1889
336
|
|
|
1890
|
-
|
|
337
|
+
define_schema do
|
|
338
|
+
property :items, T::Array[T::AnyOf[ProductA, ProductB]]
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
```
|
|
1891
342
|
|
|
1892
|
-
|
|
343
|
+
---
|
|
1893
344
|
|
|
1894
|
-
|
|
345
|
+
## Validations
|
|
1895
346
|
|
|
1896
|
-
###
|
|
347
|
+
### Automatic validations (default)
|
|
1897
348
|
|
|
1898
|
-
|
|
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
|
|
349
|
+
EasyTalk can generate ActiveModel validations from constraints:
|
|
1903
350
|
|
|
1904
|
-
|
|
351
|
+
```ruby
|
|
352
|
+
EasyTalk.configure do |config|
|
|
353
|
+
config.auto_validations = true
|
|
354
|
+
end
|
|
355
|
+
```
|
|
1905
356
|
|
|
1906
|
-
|
|
357
|
+
Disable globally:
|
|
1907
358
|
|
|
1908
359
|
```ruby
|
|
1909
360
|
EasyTalk.configure do |config|
|
|
1910
|
-
config.
|
|
361
|
+
config.auto_validations = false
|
|
1911
362
|
end
|
|
1912
363
|
```
|
|
1913
364
|
|
|
1914
|
-
|
|
365
|
+
When auto validations are off, you can still write validations manually:
|
|
1915
366
|
|
|
1916
367
|
```ruby
|
|
1917
368
|
class User
|
|
1918
369
|
include EasyTalk::Model
|
|
1919
370
|
|
|
371
|
+
validates :name, presence: true, length: { minimum: 2 }
|
|
372
|
+
|
|
1920
373
|
define_schema do
|
|
1921
|
-
property :name, String
|
|
374
|
+
property :name, String, min_length: 2
|
|
1922
375
|
end
|
|
1923
376
|
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
377
|
```
|
|
1934
378
|
|
|
1935
|
-
### Per-
|
|
379
|
+
### Per-model validation control
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
class LegacyModel
|
|
383
|
+
include EasyTalk::Model
|
|
384
|
+
|
|
385
|
+
define_schema(validations: false) do
|
|
386
|
+
property :data, String, min_length: 1 # no validation generated
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
1936
390
|
|
|
1937
|
-
|
|
391
|
+
### Per-property validation control
|
|
1938
392
|
|
|
1939
393
|
```ruby
|
|
1940
394
|
class User
|
|
1941
395
|
include EasyTalk::Model
|
|
1942
396
|
|
|
1943
397
|
define_schema do
|
|
1944
|
-
|
|
1945
|
-
property :
|
|
1946
|
-
property :email, String
|
|
398
|
+
property :name, String, min_length: 2
|
|
399
|
+
property :legacy_field, String, validate: false
|
|
1947
400
|
end
|
|
1948
401
|
end
|
|
1949
|
-
|
|
1950
|
-
User.json_schema
|
|
1951
|
-
# => {
|
|
1952
|
-
# "$id" => "https://example.com/schemas/user.schema.json",
|
|
1953
|
-
# "type" => "object",
|
|
1954
|
-
# ...
|
|
1955
|
-
# }
|
|
1956
402
|
```
|
|
1957
403
|
|
|
1958
|
-
###
|
|
404
|
+
### Validation adapters
|
|
1959
405
|
|
|
1960
|
-
|
|
406
|
+
EasyTalk uses a pluggable adapter system:
|
|
1961
407
|
|
|
1962
408
|
```ruby
|
|
1963
409
|
EasyTalk.configure do |config|
|
|
1964
|
-
config.
|
|
410
|
+
config.validation_adapter = :active_model # default
|
|
411
|
+
# config.validation_adapter = :none # disable validation generation
|
|
1965
412
|
end
|
|
413
|
+
```
|
|
1966
414
|
|
|
1967
|
-
|
|
1968
|
-
include EasyTalk::Model
|
|
415
|
+
---
|
|
1969
416
|
|
|
1970
|
-
|
|
1971
|
-
schema_id :none # No $id for this model
|
|
1972
|
-
property :data, String
|
|
1973
|
-
end
|
|
1974
|
-
end
|
|
417
|
+
## Error formatting
|
|
1975
418
|
|
|
1976
|
-
|
|
1977
|
-
# => {
|
|
1978
|
-
# "type" => "object",
|
|
1979
|
-
# "properties" => { "data" => { "type" => "string" } },
|
|
1980
|
-
# ...
|
|
1981
|
-
# }
|
|
1982
|
-
# Note: No "$id" key present
|
|
1983
|
-
```
|
|
419
|
+
Instance helpers:
|
|
1984
420
|
|
|
1985
|
-
|
|
421
|
+
```ruby
|
|
422
|
+
user.validation_errors_flat
|
|
423
|
+
user.validation_errors_json_pointer
|
|
424
|
+
user.validation_errors_rfc7807
|
|
425
|
+
user.validation_errors_jsonapi
|
|
426
|
+
```
|
|
1986
427
|
|
|
1987
|
-
|
|
428
|
+
Format directly:
|
|
1988
429
|
|
|
1989
430
|
```ruby
|
|
1990
|
-
|
|
1991
|
-
|
|
431
|
+
EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: "User Validation Failed")
|
|
432
|
+
```
|
|
1992
433
|
|
|
1993
|
-
|
|
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
|
|
434
|
+
Global defaults:
|
|
2000
435
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
# }
|
|
436
|
+
```ruby
|
|
437
|
+
EasyTalk.configure do |config|
|
|
438
|
+
config.default_error_format = :rfc7807
|
|
439
|
+
config.error_type_base_uri = "https://api.example.com/errors"
|
|
440
|
+
config.include_error_codes = true
|
|
441
|
+
end
|
|
2008
442
|
```
|
|
2009
443
|
|
|
2010
|
-
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Schema-only mode
|
|
2011
447
|
|
|
2012
|
-
|
|
448
|
+
If you want schema generation and attribute accessors **without** ActiveModel validation:
|
|
2013
449
|
|
|
2014
450
|
```ruby
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
end
|
|
451
|
+
class ApiContract
|
|
452
|
+
include EasyTalk::Schema
|
|
2018
453
|
|
|
2019
|
-
class Address
|
|
2020
|
-
include EasyTalk::Model
|
|
2021
454
|
define_schema do
|
|
2022
|
-
|
|
455
|
+
title "API Contract"
|
|
456
|
+
property :name, String, min_length: 2
|
|
457
|
+
property :age, Integer, minimum: 0
|
|
2023
458
|
end
|
|
2024
459
|
end
|
|
2025
460
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
define_schema do
|
|
2029
|
-
property :name, String
|
|
2030
|
-
property :address, Address
|
|
2031
|
-
end
|
|
2032
|
-
end
|
|
461
|
+
ApiContract.json_schema
|
|
462
|
+
contract = ApiContract.new(name: "Test", age: 25)
|
|
2033
463
|
|
|
2034
|
-
|
|
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
|
-
# }
|
|
464
|
+
# No validations available:
|
|
465
|
+
# contract.valid? # => NoMethodError
|
|
2048
466
|
```
|
|
2049
467
|
|
|
2050
|
-
|
|
468
|
+
Use this for documentation, OpenAPI generation, or when validation happens elsewhere.
|
|
2051
469
|
|
|
2052
|
-
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Configuration highlights
|
|
2053
473
|
|
|
2054
474
|
```ruby
|
|
2055
|
-
|
|
2056
|
-
|
|
475
|
+
EasyTalk.configure do |config|
|
|
476
|
+
# Schema behavior
|
|
477
|
+
config.default_additional_properties = false
|
|
478
|
+
config.nilable_is_optional = false
|
|
479
|
+
config.schema_version = :none
|
|
480
|
+
config.schema_id = nil
|
|
481
|
+
config.use_refs = false
|
|
482
|
+
config.base_schema_uri = nil # Base URI for auto-generating $id
|
|
483
|
+
config.auto_generate_ids = false # Auto-generate $id from base_schema_uri
|
|
484
|
+
config.prefer_external_refs = false # Use external URI in $ref when available
|
|
485
|
+
config.property_naming_strategy = :identity # :snake_case, :camel_case, :pascal_case
|
|
2057
486
|
|
|
2058
|
-
#
|
|
2059
|
-
|
|
487
|
+
# Validations
|
|
488
|
+
config.auto_validations = true
|
|
489
|
+
config.validation_adapter = :active_model
|
|
2060
490
|
|
|
2061
|
-
#
|
|
2062
|
-
|
|
491
|
+
# Error formatting
|
|
492
|
+
config.default_error_format = :flat # :flat, :json_pointer, :rfc7807, :jsonapi
|
|
493
|
+
config.error_type_base_uri = "about:blank"
|
|
494
|
+
config.include_error_codes = true
|
|
495
|
+
end
|
|
2063
496
|
```
|
|
2064
497
|
|
|
2065
|
-
|
|
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.
|
|
498
|
+
---
|
|
2072
499
|
|
|
2073
|
-
|
|
500
|
+
## Advanced topics
|
|
2074
501
|
|
|
2075
|
-
|
|
502
|
+
For more detailed documentation, see the [full API reference on RubyDoc](https://rubydoc.info/gems/easy_talk).
|
|
2076
503
|
|
|
2077
|
-
|
|
504
|
+
### JSON Schema drafts, `$id`, and `$ref`
|
|
2078
505
|
|
|
2079
|
-
|
|
506
|
+
EasyTalk can emit `$schema` for multiple drafts (Draft-04 through 2020-12), supports `$id`, and can use `$ref`/`$defs` for reusable definitions:
|
|
2080
507
|
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
- Improves schema readability and maintainability
|
|
2089
|
-
- Aligns with JSON Schema best practices for reusable definitions
|
|
508
|
+
```ruby
|
|
509
|
+
EasyTalk.configure do |config|
|
|
510
|
+
config.schema_version = :draft202012
|
|
511
|
+
config.schema_id = "https://example.com/schemas/user.json"
|
|
512
|
+
config.use_refs = true # Use $ref/$defs for nested models
|
|
513
|
+
end
|
|
514
|
+
```
|
|
2090
515
|
|
|
2091
|
-
|
|
516
|
+
#### External schema references
|
|
2092
517
|
|
|
2093
|
-
|
|
518
|
+
Use external URIs in `$ref` for modular, reusable schemas:
|
|
2094
519
|
|
|
2095
520
|
```ruby
|
|
521
|
+
EasyTalk.configure do |config|
|
|
522
|
+
config.use_refs = true
|
|
523
|
+
config.prefer_external_refs = true
|
|
524
|
+
config.base_schema_uri = 'https://example.com/schemas'
|
|
525
|
+
config.auto_generate_ids = true
|
|
526
|
+
end
|
|
527
|
+
|
|
2096
528
|
class Address
|
|
2097
529
|
include EasyTalk::Model
|
|
530
|
+
|
|
2098
531
|
define_schema do
|
|
2099
532
|
property :street, String
|
|
2100
533
|
property :city, String
|
|
2101
534
|
end
|
|
2102
535
|
end
|
|
2103
536
|
|
|
2104
|
-
class
|
|
537
|
+
class Customer
|
|
2105
538
|
include EasyTalk::Model
|
|
539
|
+
|
|
2106
540
|
define_schema do
|
|
2107
541
|
property :name, String
|
|
2108
542
|
property :address, Address
|
|
2109
543
|
end
|
|
2110
544
|
end
|
|
2111
545
|
|
|
2112
|
-
|
|
2113
|
-
# =>
|
|
2114
|
-
#
|
|
2115
|
-
#
|
|
2116
|
-
#
|
|
2117
|
-
#
|
|
2118
|
-
#
|
|
2119
|
-
#
|
|
2120
|
-
#
|
|
2121
|
-
#
|
|
2122
|
-
#
|
|
2123
|
-
#
|
|
2124
|
-
#
|
|
2125
|
-
# },
|
|
2126
|
-
# ...
|
|
2127
|
-
# }
|
|
2128
|
-
```
|
|
2129
|
-
|
|
2130
|
-
### Enabling `$ref` References
|
|
2131
|
-
|
|
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
|
|
2139
|
-
end
|
|
2140
|
-
```
|
|
2141
|
-
|
|
2142
|
-
With this configuration, nested models are referenced via `$ref` and their definitions are placed in `$defs`:
|
|
2143
|
-
|
|
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
|
-
# }
|
|
546
|
+
Customer.json_schema
|
|
547
|
+
# =>
|
|
548
|
+
# {
|
|
549
|
+
# "properties": {
|
|
550
|
+
# "address": { "$ref": "https://example.com/schemas/address" }
|
|
551
|
+
# },
|
|
552
|
+
# "$defs": {
|
|
553
|
+
# "Address": {
|
|
554
|
+
# "$id": "https://example.com/schemas/address",
|
|
555
|
+
# "properties": { "street": {...}, "city": {...} }
|
|
556
|
+
# }
|
|
557
|
+
# }
|
|
558
|
+
# }
|
|
2164
559
|
```
|
|
2165
560
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
You can also enable `$ref` for specific properties using the `ref: true` constraint:
|
|
561
|
+
**Explicit schema IDs:**
|
|
2169
562
|
|
|
2170
563
|
```ruby
|
|
2171
|
-
class
|
|
564
|
+
class Address
|
|
2172
565
|
include EasyTalk::Model
|
|
566
|
+
|
|
2173
567
|
define_schema do
|
|
2174
|
-
|
|
2175
|
-
property :
|
|
568
|
+
schema_id 'https://example.com/schemas/address'
|
|
569
|
+
property :street, String
|
|
2176
570
|
end
|
|
2177
571
|
end
|
|
2178
572
|
```
|
|
2179
573
|
|
|
2180
|
-
|
|
574
|
+
**Per-property ref control:**
|
|
2181
575
|
|
|
2182
576
|
```ruby
|
|
2183
|
-
|
|
2184
|
-
config.use_refs = true
|
|
2185
|
-
end
|
|
2186
|
-
|
|
2187
|
-
class Person
|
|
577
|
+
class Customer
|
|
2188
578
|
include EasyTalk::Model
|
|
579
|
+
|
|
2189
580
|
define_schema do
|
|
2190
|
-
property :
|
|
2191
|
-
property :
|
|
581
|
+
property :address, Address, ref: false # Inline instead of ref
|
|
582
|
+
property :billing, Address # Uses ref (global setting)
|
|
2192
583
|
end
|
|
2193
584
|
end
|
|
2194
585
|
```
|
|
2195
586
|
|
|
2196
|
-
###
|
|
587
|
+
### Additional properties with types
|
|
2197
588
|
|
|
2198
|
-
|
|
589
|
+
Beyond boolean values, `additional_properties` now supports type constraints for dynamic properties:
|
|
2199
590
|
|
|
2200
591
|
```ruby
|
|
2201
|
-
|
|
2202
|
-
config.use_refs = true
|
|
2203
|
-
end
|
|
2204
|
-
|
|
2205
|
-
class Company
|
|
592
|
+
class Config
|
|
2206
593
|
include EasyTalk::Model
|
|
594
|
+
|
|
2207
595
|
define_schema do
|
|
2208
596
|
property :name, String
|
|
2209
|
-
|
|
597
|
+
|
|
598
|
+
# Allow any string-typed additional properties
|
|
599
|
+
additional_properties String
|
|
2210
600
|
end
|
|
2211
601
|
end
|
|
2212
602
|
|
|
2213
|
-
|
|
2214
|
-
#
|
|
2215
|
-
|
|
2216
|
-
#
|
|
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
|
|
603
|
+
config = Config.new(name: 'app')
|
|
604
|
+
config.label = 'Production' # Dynamic property
|
|
605
|
+
config.as_json
|
|
606
|
+
# => { 'name' => 'app', 'label' => 'Production' }
|
|
2234
607
|
```
|
|
2235
608
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
When using `$ref` with nilable model types, EasyTalk uses `anyOf` to combine the reference with the null type:
|
|
609
|
+
**With constraints:**
|
|
2239
610
|
|
|
2240
611
|
```ruby
|
|
2241
|
-
|
|
2242
|
-
config.use_refs = true
|
|
2243
|
-
end
|
|
2244
|
-
|
|
2245
|
-
class Person
|
|
612
|
+
class StrictConfig
|
|
2246
613
|
include EasyTalk::Model
|
|
614
|
+
|
|
2247
615
|
define_schema do
|
|
2248
|
-
property :
|
|
2249
|
-
|
|
616
|
+
property :id, Integer
|
|
617
|
+
# Integer values between 0 and 100 only
|
|
618
|
+
additional_properties Integer, minimum: 0, maximum: 100
|
|
2250
619
|
end
|
|
2251
620
|
end
|
|
2252
621
|
|
|
2253
|
-
|
|
2254
|
-
# =>
|
|
2255
|
-
#
|
|
2256
|
-
#
|
|
2257
|
-
#
|
|
2258
|
-
#
|
|
2259
|
-
#
|
|
2260
|
-
#
|
|
2261
|
-
#
|
|
2262
|
-
#
|
|
2263
|
-
# }
|
|
2264
|
-
# },
|
|
2265
|
-
# "$defs" => {
|
|
2266
|
-
# "Address" => { ... }
|
|
2267
|
-
# },
|
|
2268
|
-
# ...
|
|
2269
|
-
# }
|
|
622
|
+
StrictConfig.json_schema
|
|
623
|
+
# =>
|
|
624
|
+
# {
|
|
625
|
+
# "properties": { "id": { "type": "integer" } },
|
|
626
|
+
# "additionalProperties": {
|
|
627
|
+
# "type": "integer",
|
|
628
|
+
# "minimum": 0,
|
|
629
|
+
# "maximum": 100
|
|
630
|
+
# }
|
|
631
|
+
# }
|
|
2270
632
|
```
|
|
2271
633
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
When the same model is used multiple times, it only appears once in `$defs`:
|
|
634
|
+
**Nested models as additional properties:**
|
|
2275
635
|
|
|
2276
636
|
```ruby
|
|
2277
637
|
class Person
|
|
2278
638
|
include EasyTalk::Model
|
|
639
|
+
|
|
2279
640
|
define_schema do
|
|
2280
641
|
property :name, String
|
|
2281
|
-
|
|
2282
|
-
property :work_address, Address, ref: true
|
|
2283
|
-
property :shipping_addresses, T::Array[Address], ref: true
|
|
642
|
+
additional_properties Address # All additional properties must be Address objects
|
|
2284
643
|
end
|
|
2285
644
|
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
645
|
```
|
|
2305
646
|
|
|
2306
|
-
###
|
|
647
|
+
### Object-level constraints
|
|
2307
648
|
|
|
2308
|
-
|
|
649
|
+
Apply schema-wide constraints to limit or validate object structure:
|
|
2309
650
|
|
|
2310
651
|
```ruby
|
|
2311
|
-
class
|
|
652
|
+
class StrictObject
|
|
2312
653
|
include EasyTalk::Model
|
|
654
|
+
|
|
2313
655
|
define_schema do
|
|
2314
|
-
property :
|
|
656
|
+
property :required1, String
|
|
657
|
+
property :required2, String
|
|
658
|
+
property :optional1, String, optional: true
|
|
659
|
+
property :optional2, String, optional: true
|
|
660
|
+
|
|
661
|
+
# Require at least 2 properties
|
|
662
|
+
min_properties 2
|
|
663
|
+
# Allow at most 3 properties
|
|
664
|
+
max_properties 3
|
|
2315
665
|
end
|
|
2316
666
|
end
|
|
2317
667
|
|
|
2318
|
-
|
|
2319
|
-
# =>
|
|
2320
|
-
# "$ref" => "#/$defs/Address",
|
|
2321
|
-
# "description" => "Primary address",
|
|
2322
|
-
# "title" => "Main Address"
|
|
2323
|
-
# }
|
|
668
|
+
obj = StrictObject.new(required1: 'a')
|
|
669
|
+
obj.valid? # => false (only 1 property, needs at least 2)
|
|
2324
670
|
```
|
|
2325
671
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
When using `compose` with `T::AllOf`, `T::AnyOf`, or `T::OneOf`, the composed models are also placed in `$defs`:
|
|
672
|
+
**Pattern properties:**
|
|
2329
673
|
|
|
2330
674
|
```ruby
|
|
2331
|
-
class
|
|
675
|
+
class DynamicConfig
|
|
2332
676
|
include EasyTalk::Model
|
|
677
|
+
|
|
2333
678
|
define_schema do
|
|
2334
|
-
|
|
2335
|
-
|
|
679
|
+
property :name, String
|
|
680
|
+
|
|
681
|
+
# Properties matching /^env_/ must be strings
|
|
682
|
+
pattern_properties(
|
|
683
|
+
'^env_' => { type: 'string' }
|
|
684
|
+
)
|
|
2336
685
|
end
|
|
2337
686
|
end
|
|
2338
687
|
```
|
|
2339
688
|
|
|
2340
|
-
|
|
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.
|
|
689
|
+
**Dependent required:**
|
|
2347
690
|
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
### Default Behavior
|
|
691
|
+
```ruby
|
|
692
|
+
class ShippingInfo
|
|
693
|
+
include EasyTalk::Model
|
|
2353
694
|
|
|
2354
|
-
|
|
695
|
+
define_schema do
|
|
696
|
+
property :name, String
|
|
697
|
+
property :credit_card, String, optional: true
|
|
698
|
+
property :billing_address, String, optional: true
|
|
2355
699
|
|
|
2356
|
-
|
|
700
|
+
# If credit_card is present, billing_address is required
|
|
701
|
+
dependent_required(
|
|
702
|
+
'credit_card' => ['billing_address']
|
|
703
|
+
)
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
```
|
|
2357
707
|
|
|
2358
|
-
###
|
|
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).
|
|
708
|
+
### Custom type builders
|
|
2360
709
|
|
|
2361
|
-
|
|
710
|
+
Register custom types with their own schema builders:
|
|
2362
711
|
|
|
2363
|
-
|
|
2364
|
-
|
|
712
|
+
```ruby
|
|
713
|
+
EasyTalk.configure do |config|
|
|
714
|
+
config.register_type(Money, MoneySchemaBuilder)
|
|
715
|
+
end
|
|
2365
716
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
- Complex composition scenarios may require manual adjustment
|
|
717
|
+
# Or directly:
|
|
718
|
+
EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
|
|
719
|
+
```
|
|
2370
720
|
|
|
2371
|
-
|
|
721
|
+
See the [Custom Type Builders documentation](https://rubydoc.info/gems/easy_talk/EasyTalk/Builders/Registry) for details on creating builders.
|
|
2372
722
|
|
|
2373
|
-
|
|
723
|
+
---
|
|
2374
724
|
|
|
2375
|
-
|
|
725
|
+
## Known limitations
|
|
2376
726
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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) |
|
|
727
|
+
EasyTalk aims to produce broadly compatible JSON Schema, but:
|
|
728
|
+
- Some draft-specific keywords/features may require manual schema tweaks
|
|
729
|
+
- Custom formats are limited (extend via custom builders when needed)
|
|
730
|
+
- Extremely complex composition can outgrow “auto validations” and may need manual validations or external schema validators
|
|
2383
731
|
|
|
2384
|
-
|
|
732
|
+
---
|
|
2385
733
|
|
|
2386
|
-
|
|
734
|
+
## Contributing
|
|
2387
735
|
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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 |
|
|
736
|
+
- Run `bin/setup`
|
|
737
|
+
- Run specs: `bundle exec rake spec`
|
|
738
|
+
- Run lint: `bundle exec rubocop`
|
|
2401
739
|
|
|
2402
|
-
|
|
740
|
+
Bug reports and PRs welcome.
|
|
2403
741
|
|
|
2404
|
-
|
|
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) |
|
|
742
|
+
---
|
|
2411
743
|
|
|
2412
744
|
## License
|
|
2413
745
|
|
|
2414
|
-
|
|
746
|
+
MIT
|