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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -43
  3. data/CHANGELOG.md +89 -0
  4. data/README.md +447 -2115
  5. data/docs/json_schema_compliance.md +140 -26
  6. data/docs/primitive-schema-rfc.md +894 -0
  7. data/lib/easy_talk/builders/base_builder.rb +2 -1
  8. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  9. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  10. data/lib/easy_talk/builders/composition_builder.rb +7 -2
  11. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  12. data/lib/easy_talk/builders/null_builder.rb +4 -1
  13. data/lib/easy_talk/builders/number_builder.rb +4 -1
  14. data/lib/easy_talk/builders/object_builder.rb +64 -3
  15. data/lib/easy_talk/builders/registry.rb +15 -1
  16. data/lib/easy_talk/builders/string_builder.rb +3 -1
  17. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  18. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +4 -2
  20. data/lib/easy_talk/builders/union_builder.rb +5 -1
  21. data/lib/easy_talk/configuration.rb +17 -2
  22. data/lib/easy_talk/errors.rb +1 -0
  23. data/lib/easy_talk/errors_helper.rb +3 -0
  24. data/lib/easy_talk/json_schema_equality.rb +46 -0
  25. data/lib/easy_talk/keywords.rb +0 -1
  26. data/lib/easy_talk/model.rb +27 -1
  27. data/lib/easy_talk/model_helper.rb +4 -0
  28. data/lib/easy_talk/naming_strategies.rb +4 -0
  29. data/lib/easy_talk/property.rb +7 -0
  30. data/lib/easy_talk/ref_helper.rb +6 -0
  31. data/lib/easy_talk/schema.rb +1 -0
  32. data/lib/easy_talk/schema_definition.rb +52 -6
  33. data/lib/easy_talk/schema_methods.rb +36 -5
  34. data/lib/easy_talk/sorbet_extension.rb +1 -0
  35. data/lib/easy_talk/type_introspection.rb +45 -1
  36. data/lib/easy_talk/types/tuple.rb +77 -0
  37. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +350 -62
  38. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  39. data/lib/easy_talk/validation_adapters/base.rb +12 -0
  40. data/lib/easy_talk/validation_adapters/none_adapter.rb +9 -0
  41. data/lib/easy_talk/validation_builder.rb +1 -0
  42. data/lib/easy_talk/version.rb +1 -1
  43. data/lib/easy_talk.rb +1 -0
  44. metadata +13 -4
data/README.md CHANGED
@@ -2,2413 +2,745 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/easy_talk.svg)](https://badge.fury.io/rb/easy_talk)
4
4
  [![Ruby](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml/badge.svg)](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml)
5
+ [![codecov](https://codecov.io/gh/sergiobayona/easy_talk/graph/badge.svg)](https://codecov.io/gh/sergiobayona/easy_talk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Ruby](https://img.shields.io/badge/ruby-3.2%2B-ruby.svg)](https://www.ruby-lang.org)
8
+ [![Downloads](https://img.shields.io/gem/dt/easy_talk.svg)](https://rubygems.org/gems/easy_talk)
5
9
  [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/easy_talk)
10
+ [![GitHub stars](https://img.shields.io/github/stars/sergiobayona/easy_talk?style=social)](https://github.com/sergiobayona/easy_talk)
6
11
 
7
- ## Table of Contents
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
- ## Installation
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
- ### Arrays and Collections
461
- Arrays can be defined using the `T::Array` type:
16
+ ---
462
17
 
463
- ```ruby
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 also define arrays of complex types:
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
- ```ruby
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
- ### Constraints and Automatic Validations
475
- Constraints are added to properties and are used for both schema generation and automatic validation generation:
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
- ```ruby
478
- define_schema do
479
- property :name, String, min_length: 2, max_length: 50
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 :category, String, enum: ["A", "B", "C"]
482
- property :score, Float, minimum: 0.0, maximum: 100.0
483
- property :tags, T::Array[String], min_items: 1, max_items: 10
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
- ## Schema Composition
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
- ### Using T::AnyOf
550
- The `T::AnyOf` type allows a property to match any of the specified schemas:
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
- define_schema do
557
- property :details, T::AnyOf[CreditCard, Paypal, BankTransfer]
558
- end
559
- end
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
- ### Using T::OneOf
563
- The `T::OneOf` type requires a property to match exactly one of the specified schemas:
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
- ```ruby
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
- define_schema do
570
- property :contact, T::OneOf[PhoneContact, EmailContact]
571
- end
572
- end
573
- ```
60
+ ---
574
61
 
575
- ### Using T::AllOf
576
- The `T::AllOf` type requires a property to match all of the specified schemas:
62
+ ## Installation
577
63
 
578
- ```ruby
579
- class VehicleRegistration
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
- ### Array Composition
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
- class ProductA
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
- This generates a JSON Schema where the `items` array validates each element against `oneOf`:
73
+ Then:
620
74
 
621
- ```json
622
- {
623
- "properties": {
624
- "items": {
625
- "type": "array",
626
- "items": {
627
- "oneOf": [
628
- { "type": "object", "properties": { "sku": {...}, "weight": {...} }, ... },
629
- { "type": "object", "properties": { "sku": {...}, "digital_url": {...} }, ... }
630
- ]
631
- }
632
- }
633
- }
634
- }
75
+ ```bash
76
+ bundle install
635
77
  ```
636
78
 
637
- You can use any composition type with arrays:
638
- - `T::Array[T::OneOf[A, B]]` - each item matches exactly one schema
639
- - `T::Array[T::AnyOf[A, B]]` - each item matches one or more schemas
640
- - `T::Array[T::AllOf[A, B]]` - each item matches all schemas
79
+ ---
641
80
 
642
- ### Complex Compositions
643
- You can combine composition types to create complex schemas:
81
+ ## Quick start
644
82
 
645
83
  ```ruby
646
- class ComplexObject
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
- property :name, String, min_length: 2 # Auto-generates presence + length validations
697
- property :age, Integer, minimum: 18 # Auto-generates presence + numericality validations
698
- property :height, Float # Auto-generates presence validation (overridden above)
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
- # Required but nullable (must exist, can be null)
1537
- property :age, T.nilable(Integer)
1538
-
1539
- # Optional but not nullable (can be omitted, cannot be null if present)
1540
- property :email, String, optional: true
1541
-
1542
- # Optional and nullable (can be omitted, can be null if present)
1543
- nullable_optional_property :bio, String
1544
- end
1545
- end
1546
- ```
1547
-
1548
- This creates clear expectations for data validation:
1549
- - `id` and `name` must be present and cannot be null
1550
- - `age` must be present but can be null
1551
- - `email` doesn't have to be present, but if it is, it cannot be null
1552
- - `bio` doesn't have to be present, and if it is, it can be null
1553
-
1554
- ### Common Gotchas
1555
-
1556
- #### Misconception: Nullable Implies Optional
1557
-
1558
- A common mistake is assuming that `T.nilable(Type)` makes a property optional. By default, it only allows the property to have a null value - the property itself is still required to exist in the object.
1559
-
1560
- #### Misconception: Optional Properties Accept Null
1561
-
1562
- An optional property (defined with `optional: true`) can be omitted entirely, but if it is present, it must conform to its type constraint. If you want to allow null values, you must also make it nullable with `T.nilable(Type)`.
1563
-
1564
- ### Migration from Earlier Versions
1565
-
1566
- If you're upgrading from EasyTalk version 1.0.1 or earlier, be aware that the handling of nullable vs optional properties has been improved for clarity.
1567
-
1568
- To maintain backward compatibility with your existing code, you can use:
1569
-
1570
- ```ruby
1571
- EasyTalk.configure do |config|
1572
- config.nilable_is_optional = true # Makes T.nilable properties behave as they did before
1573
- end
1574
- ```
1575
-
1576
- We recommend updating your schema definitions to explicitly declare which properties are optional using the `optional: true` constraint, as this makes your intent clearer.
1577
-
1578
- ### Best Practices
1579
-
1580
- 1. **Be explicit about intent**: Always clarify whether properties should be nullable, optional, or both
1581
- 2. **Use the helper method**: For properties that are both nullable and optional, use `nullable_optional_property`
1582
- 3. **Document expectations**: Use comments to clarify validation requirements for complex schemas
1583
- 4. **Consider validation implications**: Remember that ActiveModel validations operate independently of the schema definition
1584
-
1585
- ### JSON Schema Comparison
1586
-
1587
- | EasyTalk Definition | Required | Nullable | JSON Schema Equivalent |
1588
- |--------------------|----------|----------|------------------------|
1589
- | `property :p, String` | Yes | No | `{ "properties": { "p": { "type": "string" } }, "required": ["p"] }` |
1590
- | `property :p, T.nilable(String)` | Yes | Yes | `{ "properties": { "p": { "type": ["string", "null"] } }, "required": ["p"] }` |
1591
- | `property :p, String, optional: true` | No | No | `{ "properties": { "p": { "type": "string" } } }` |
1592
- | `nullable_optional_property :p, String` | No | Yes | `{ "properties": { "p": { "type": ["string", "null"] } } }` |
1593
-
1594
- ## Migration Guide from v1.x to v2.0
1595
-
1596
- ### Breaking Changes Summary
1597
-
1598
- 1. **Removed Block-Style Sub-Schemas**: Hash-based nested definitions are no longer supported
1599
- 2. **Enhanced Validation System**: Automatic validation generation is now enabled by default
1600
- 3. **Improved Model Initialization**: Better support for nested model instantiation
1601
-
1602
- ### Migration Steps
1603
-
1604
- #### 1. Replace Hash-based Nested Schemas
1605
-
1606
- ```ruby
1607
- # OLD (v1.x) - No longer works
1608
- class User
1609
- include EasyTalk::Model
1610
- define_schema do
1611
- property :address, Hash do
1612
- property :street, String
1613
- property :city, String
1614
- end
1615
- end
1616
- end
1617
-
1618
- # NEW (v2.x) - Extract to separate classes
1619
- class Address
1620
- include EasyTalk::Model
1621
- define_schema do
1622
- property :street, String
1623
- property :city, String
1624
- end
1625
- end
1626
-
1627
- class User
1628
- include EasyTalk::Model
1629
- define_schema do
1630
- property :address, Address
1631
- end
1632
- end
1633
- ```
1634
-
1635
- #### 2. Review Automatic Validations
1636
-
1637
- With `auto_validations: true` (default), you may need to remove redundant manual validations:
1638
-
1639
- ```ruby
1640
- # OLD (v1.x) - Manual validations required
1641
- class User
1642
- include EasyTalk::Model
1643
-
1644
- validates :name, presence: true, length: { minimum: 2 }
1645
- validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
1646
-
1647
- define_schema do
1648
- property :name, String
1649
- property :email, String
1650
- end
1651
- end
1652
-
1653
- # NEW (v2.x) - Automatic validations from constraints
1654
- class User
1655
- include EasyTalk::Model
1656
-
1657
- # Only add validations not covered by schema constraints
1658
- validates :email, uniqueness: true
1659
-
1660
- define_schema do
1661
- property :name, String, min_length: 2 # Auto-generates presence + length
1662
- property :email, String, format: "email" # Auto-generates presence + format
1663
- end
1664
- end
1665
- ```
1666
-
1667
- #### 3. Configuration Updates
1668
-
1669
- Review your configuration for new options:
1670
-
1671
- ```ruby
1672
- EasyTalk.configure do |config|
1673
- # New option in v2.0
1674
- config.auto_validations = true # Enable/disable automatic validation generation
1675
-
1676
- # Existing options (unchanged)
1677
- config.nilable_is_optional = false
1678
- config.default_additional_properties = false
1679
- # ... other existing config
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
- ### Compatibility Notes
1684
-
1685
- - **Ruby Version**: Still requires Ruby 3.2+
1686
- - **Dependencies**: Core dependencies remain the same
1687
- - **JSON Schema Output**: No changes to generated schemas
1688
- - **ActiveModel Integration**: Fully backward compatible
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
- ## Development and Contributing
106
+ **Generated JSON Schema:**
1691
107
 
1692
- ### Setting Up the Development Environment
1693
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that lets you experiment.
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
- To install this gem onto your local machine, run:
123
+ ---
1696
124
 
1697
- ```bash
1698
- bundle exec rake install
1699
- ```
125
+ ## Property constraints
1700
126
 
1701
- ### Running Tests
1702
- Run the test suite with:
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
- ```bash
1705
- bundle exec rake spec
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
- ### Code Quality
1709
- Run the linter:
147
+ When `auto_validations` is enabled (default), these constraints automatically generate corresponding ActiveModel validations.
1710
148
 
1711
- ```bash
1712
- bundle exec rubocop
1713
- ```
149
+ ---
1714
150
 
1715
- ### Contributing Guidelines
1716
- Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
151
+ ## Core concepts
1717
152
 
1718
- ## JSON Schema Version (`$schema` Keyword)
153
+ ### Required vs optional vs nullable (don't get tricked)
1719
154
 
1720
- The `$schema` keyword declares which JSON Schema dialect (draft version) a schema conforms to. EasyTalk supports configuring this at both the global and per-model level.
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
- ### Why Use `$schema`?
159
+ EasyTalk mirrors that precisely:
1723
160
 
1724
- The `$schema` keyword:
1725
- - Declares the JSON Schema version your schema is written against
1726
- - Helps validators understand which specification to use
1727
- - Enables tooling to provide appropriate validation and autocomplete
1728
- - Documents the schema dialect for consumers of your API
161
+ ```ruby
162
+ class Profile
163
+ include EasyTalk::Model
1729
164
 
1730
- ### Supported Draft Versions
165
+ define_schema do
166
+ # required, not nullable
167
+ property :name, String
1731
168
 
1732
- EasyTalk supports the following JSON Schema draft versions:
169
+ # required, nullable (must exist, may be null)
170
+ property :age, T.nilable(Integer)
1733
171
 
1734
- | Symbol | JSON Schema Version | URI |
1735
- |--------|---------------------|-----|
1736
- | `:draft202012` | Draft 2020-12 (latest) | `https://json-schema.org/draft/2020-12/schema` |
1737
- | `:draft201909` | Draft 2019-09 | `https://json-schema.org/draft/2019-09/schema` |
1738
- | `:draft7` | Draft-07 | `http://json-schema.org/draft-07/schema#` |
1739
- | `:draft6` | Draft-06 | `http://json-schema.org/draft-06/schema#` |
1740
- | `:draft4` | Draft-04 | `http://json-schema.org/draft-04/schema#` |
1741
- | `:none` | No `$schema` output (default) | N/A |
172
+ # optional, not nullable (may be omitted, but cannot be null if present)
173
+ property :nickname, String, optional: true
1742
174
 
1743
- ### Global Configuration
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
- Configure the schema version globally to apply to all models:
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.schema_version = :draft202012 # Use JSON Schema Draft 2020-12
188
+ config.nilable_is_optional = true
1750
189
  end
1751
190
  ```
1752
191
 
1753
- With this configuration, all models will include `$schema` in their output:
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.json_schema
1765
- # => {
1766
- # "$schema" => "https://json-schema.org/draft/2020-12/schema",
1767
- # "type" => "object",
1768
- # "properties" => { "name" => { "type" => "string" } },
1769
- # "required" => ["name"],
1770
- # "additionalProperties" => false
1771
- # }
1772
- ```
217
+ user = User.new(
218
+ name: "John",
219
+ address: { street: "123 Main St", city: "Boston" } # Hash becomes Address automatically
220
+ )
1773
221
 
1774
- ### Per-Model Configuration
222
+ user.address.class # => Address
223
+ ```
1775
224
 
1776
- Override the global setting for individual models using the `schema_version` keyword in the schema definition:
225
+ Nested models inside arrays work too:
1777
226
 
1778
227
  ```ruby
1779
- class LegacyModel
228
+ class Order
1780
229
  include EasyTalk::Model
1781
230
 
1782
231
  define_schema do
1783
- schema_version :draft7 # Use Draft-07 for this specific model
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
- ### Disabling `$schema` for Specific Models
237
+ ---
1797
238
 
1798
- If you have a global schema version configured but want to exclude `$schema` from a specific model, use `:none`:
239
+ ### Tuple arrays (fixed-position types)
1799
240
 
1800
- ```ruby
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
- class InternalModel
243
+ ```ruby
244
+ class GeoLocation
1806
245
  include EasyTalk::Model
1807
246
 
1808
247
  define_schema do
1809
- schema_version :none # No $schema for this model
1810
- property :data, String
248
+ property :name, String
249
+ # Fixed: [latitude, longitude]
250
+ property :coordinates, T::Tuple[Float, Float]
1811
251
  end
1812
252
  end
1813
253
 
1814
- InternalModel.json_schema
1815
- # => {
1816
- # "type" => "object",
1817
- # "properties" => { "data" => { "type" => "string" } },
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
- ### Custom Schema URIs
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
- You can also specify a custom URI if you're using a custom meta-schema or a different schema registry:
276
+ **Mixed-type tuples:**
1826
277
 
1827
278
  ```ruby
1828
- class CustomModel
279
+ class DataRow
1829
280
  include EasyTalk::Model
1830
281
 
1831
282
  define_schema do
1832
- schema_version 'https://my-company.com/schemas/v1/meta-schema.json'
1833
- property :id, String
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
- ### Nested Models
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
- EasyTalk.configure do |config|
1844
- config.schema_version = :draft202012
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
- class Address
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 :city, String
321
+ property :sku, String
322
+ property :weight, Float
1851
323
  end
1852
324
  end
1853
325
 
1854
- class User
326
+ class ProductB
1855
327
  include EasyTalk::Model
1856
328
  define_schema do
1857
- property :name, String
1858
- property :address, Address
329
+ property :sku, String
330
+ property :color, String
1859
331
  end
1860
332
  end
1861
333
 
1862
- User.json_schema
1863
- # => {
1864
- # "$schema" => "https://json-schema.org/draft/2020-12/schema", # Only at root
1865
- # "type" => "object",
1866
- # "properties" => {
1867
- # "name" => { "type" => "string" },
1868
- # "address" => {
1869
- # "type" => "object", # No $schema here
1870
- # "properties" => { "city" => { "type" => "string" } },
1871
- # ...
1872
- # }
1873
- # },
1874
- # ...
1875
- # }
1876
- ```
1877
-
1878
- ### Default Behavior
1879
-
1880
- By default, `schema_version` is set to `:none`, meaning no `$schema` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
1881
-
1882
- ### Best Practices
1883
-
1884
- 1. **Choose a version appropriate for your validators**: If you're using a specific JSON Schema validator, check which drafts it supports.
1885
-
1886
- 2. **Use Draft 2020-12 for new projects**: It's the latest stable version with the most features.
1887
-
1888
- 3. **Be consistent**: Use global configuration for consistency across your application, and only override per-model when necessary.
334
+ class Cart
335
+ include EasyTalk::Model
1889
336
 
1890
- 4. **Consider your consumers**: If your schemas are consumed by external systems, ensure they support the draft version you're using.
337
+ define_schema do
338
+ property :items, T::Array[T::AnyOf[ProductA, ProductB]]
339
+ end
340
+ end
341
+ ```
1891
342
 
1892
- ## Schema Identifier (`$id` Keyword)
343
+ ---
1893
344
 
1894
- The `$id` keyword provides a unique identifier for your JSON Schema document. EasyTalk supports configuring this at both the global and per-model level.
345
+ ## Validations
1895
346
 
1896
- ### Why Use `$id`?
347
+ ### Automatic validations (default)
1897
348
 
1898
- The `$id` keyword:
1899
- - Establishes a unique URI identifier for the schema
1900
- - Enables referencing schemas from other documents via `$ref`
1901
- - Provides a base URI for resolving relative references within the schema
1902
- - Documents the canonical location of the schema
349
+ EasyTalk can generate ActiveModel validations from constraints:
1903
350
 
1904
- ### Global Configuration
351
+ ```ruby
352
+ EasyTalk.configure do |config|
353
+ config.auto_validations = true
354
+ end
355
+ ```
1905
356
 
1906
- Configure the schema ID globally to apply to all models:
357
+ Disable globally:
1907
358
 
1908
359
  ```ruby
1909
360
  EasyTalk.configure do |config|
1910
- config.schema_id = 'https://example.com/schemas/base.json'
361
+ config.auto_validations = false
1911
362
  end
1912
363
  ```
1913
364
 
1914
- With this configuration, all models will include `$id` in their output:
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-Model Configuration
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
- Override the global setting for individual models using the `schema_id` keyword in the schema definition:
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
- schema_id 'https://example.com/schemas/user.schema.json'
1945
- property :name, String
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
- ### Disabling `$id` for Specific Models
404
+ ### Validation adapters
1959
405
 
1960
- If you have a global schema ID configured but want to exclude `$id` from a specific model, use `:none`:
406
+ EasyTalk uses a pluggable adapter system:
1961
407
 
1962
408
  ```ruby
1963
409
  EasyTalk.configure do |config|
1964
- config.schema_id = 'https://example.com/schemas/default.json'
410
+ config.validation_adapter = :active_model # default
411
+ # config.validation_adapter = :none # disable validation generation
1965
412
  end
413
+ ```
1966
414
 
1967
- class InternalModel
1968
- include EasyTalk::Model
415
+ ---
1969
416
 
1970
- define_schema do
1971
- schema_id :none # No $id for this model
1972
- property :data, String
1973
- end
1974
- end
417
+ ## Error formatting
1975
418
 
1976
- InternalModel.json_schema
1977
- # => {
1978
- # "type" => "object",
1979
- # "properties" => { "data" => { "type" => "string" } },
1980
- # ...
1981
- # }
1982
- # Note: No "$id" key present
1983
- ```
419
+ Instance helpers:
1984
420
 
1985
- ### Combining `$schema` and `$id`
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
- When both `$schema` and `$id` are configured, they appear in the standard order (`$schema` first, then `$id`):
428
+ Format directly:
1988
429
 
1989
430
  ```ruby
1990
- class Product
1991
- include EasyTalk::Model
431
+ EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: "User Validation Failed")
432
+ ```
1992
433
 
1993
- define_schema do
1994
- schema_version :draft202012
1995
- schema_id 'https://example.com/schemas/product.schema.json'
1996
- property :name, String
1997
- property :price, Float
1998
- end
1999
- end
434
+ Global defaults:
2000
435
 
2001
- Product.json_schema
2002
- # => {
2003
- # "$schema" => "https://json-schema.org/draft/2020-12/schema",
2004
- # "$id" => "https://example.com/schemas/product.schema.json",
2005
- # "type" => "object",
2006
- # ...
2007
- # }
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
- ### Nested Models
444
+ ---
445
+
446
+ ## Schema-only mode
2011
447
 
2012
- The `$id` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$id`:
448
+ If you want schema generation and attribute accessors **without** ActiveModel validation:
2013
449
 
2014
450
  ```ruby
2015
- EasyTalk.configure do |config|
2016
- config.schema_id = 'https://example.com/schemas/user.json'
2017
- end
451
+ class ApiContract
452
+ include EasyTalk::Schema
2018
453
 
2019
- class Address
2020
- include EasyTalk::Model
2021
454
  define_schema do
2022
- property :city, String
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
- class User
2027
- include EasyTalk::Model
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
- User.json_schema
2035
- # => {
2036
- # "$id" => "https://example.com/schemas/user.json", # Only at root
2037
- # "type" => "object",
2038
- # "properties" => {
2039
- # "name" => { "type" => "string" },
2040
- # "address" => {
2041
- # "type" => "object", # No $id here
2042
- # "properties" => { "city" => { "type" => "string" } },
2043
- # ...
2044
- # }
2045
- # },
2046
- # ...
2047
- # }
464
+ # No validations available:
465
+ # contract.valid? # => NoMethodError
2048
466
  ```
2049
467
 
2050
- ### URI Formats
468
+ Use this for documentation, OpenAPI generation, or when validation happens elsewhere.
2051
469
 
2052
- The `$id` accepts various URI formats:
470
+ ---
471
+
472
+ ## Configuration highlights
2053
473
 
2054
474
  ```ruby
2055
- # Absolute URI (recommended for published schemas)
2056
- schema_id 'https://example.com/schemas/user.schema.json'
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
- # Relative URI
2059
- schema_id 'user.schema.json'
487
+ # Validations
488
+ config.auto_validations = true
489
+ config.validation_adapter = :active_model
2060
490
 
2061
- # URN format
2062
- schema_id 'urn:example:user-schema'
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
- ### Default Behavior
2066
-
2067
- By default, `schema_id` is set to `nil`, meaning no `$id` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
2068
-
2069
- ### Best Practices
2070
-
2071
- 1. **Use absolute URIs for published schemas**: This ensures global uniqueness and enables external references.
498
+ ---
2072
499
 
2073
- 2. **Follow a consistent naming convention**: For example, `https://yourdomain.com/schemas/{model-name}.schema.json`.
500
+ ## Advanced topics
2074
501
 
2075
- 3. **Keep IDs stable**: Once published, avoid changing schema IDs as external systems may reference them.
502
+ For more detailed documentation, see the [full API reference on RubyDoc](https://rubydoc.info/gems/easy_talk).
2076
503
 
2077
- 4. **Combine with `$schema`**: When publishing schemas, include both `$schema` (for validation) and `$id` (for identification).
504
+ ### JSON Schema drafts, `$id`, and `$ref`
2078
505
 
2079
- ## Schema References (`$ref` and `$defs`)
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
- The `$ref` keyword allows you to reference reusable schema definitions, reducing duplication when the same model is used in multiple places. EasyTalk supports automatic `$ref` generation for nested models.
2082
-
2083
- ### Why Use `$ref`?
2084
-
2085
- The `$ref` keyword:
2086
- - Reduces schema duplication when the same model appears multiple times
2087
- - Produces cleaner, more organized schemas
2088
- - Improves schema readability and maintainability
2089
- - Aligns with JSON Schema best practices for reusable definitions
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
- ### Default Behavior (Inline Schemas)
516
+ #### External schema references
2092
517
 
2093
- By default, EasyTalk inlines nested model schemas directly:
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 Person
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
- Person.json_schema
2113
- # => {
2114
- # "type" => "object",
2115
- # "properties" => {
2116
- # "name" => { "type" => "string" },
2117
- # "address" => {
2118
- # "type" => "object",
2119
- # "properties" => {
2120
- # "street" => { "type" => "string" },
2121
- # "city" => { "type" => "string" }
2122
- # },
2123
- # ...
2124
- # }
2125
- # },
2126
- # ...
2127
- # }
2128
- ```
2129
-
2130
- ### Enabling `$ref` References
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
- #### Per-Property Configuration
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 Person
564
+ class Address
2172
565
  include EasyTalk::Model
566
+
2173
567
  define_schema do
2174
- property :name, String
2175
- property :address, Address, ref: true # Use $ref for this property
568
+ schema_id 'https://example.com/schemas/address'
569
+ property :street, String
2176
570
  end
2177
571
  end
2178
572
  ```
2179
573
 
2180
- Or disable `$ref` for specific properties when it's enabled globally:
574
+ **Per-property ref control:**
2181
575
 
2182
576
  ```ruby
2183
- EasyTalk.configure do |config|
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 :name, String
2191
- property :address, Address, ref: false # Inline this property despite global setting
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
- ### Arrays of Models
587
+ ### Additional properties with types
2197
588
 
2198
- When using `$ref` with arrays of models, the `$ref` applies to the array items:
589
+ Beyond boolean values, `additional_properties` now supports type constraints for dynamic properties:
2199
590
 
2200
591
  ```ruby
2201
- EasyTalk.configure do |config|
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
- property :addresses, T::Array[Address]
597
+
598
+ # Allow any string-typed additional properties
599
+ additional_properties String
2210
600
  end
2211
601
  end
2212
602
 
2213
- Company.json_schema
2214
- # => {
2215
- # "type" => "object",
2216
- # "properties" => {
2217
- # "name" => { "type" => "string" },
2218
- # "addresses" => {
2219
- # "type" => "array",
2220
- # "items" => { "$ref" => "#/$defs/Address" }
2221
- # }
2222
- # },
2223
- # "$defs" => {
2224
- # "Address" => { ... }
2225
- # },
2226
- # ...
2227
- # }
2228
- ```
2229
-
2230
- You can also use the per-property `ref` constraint with arrays:
2231
-
2232
- ```ruby
2233
- property :addresses, T::Array[Address], ref: true
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
- ### Nilable Models with `$ref`
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
- EasyTalk.configure do |config|
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 :name, String
2249
- property :address, T.nilable(Address)
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
- Person.json_schema
2254
- # => {
2255
- # "type" => "object",
2256
- # "properties" => {
2257
- # "name" => { "type" => "string" },
2258
- # "address" => {
2259
- # "anyOf" => [
2260
- # { "$ref" => "#/$defs/Address" },
2261
- # { "type" => "null" }
2262
- # ]
2263
- # }
2264
- # },
2265
- # "$defs" => {
2266
- # "Address" => { ... }
2267
- # },
2268
- # ...
2269
- # }
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
- ### Multiple References to the Same Model
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
- property :home_address, Address, ref: true
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
- ### Combining `$ref` with Other Constraints
647
+ ### Object-level constraints
2307
648
 
2308
- You can add additional constraints alongside `$ref`:
649
+ Apply schema-wide constraints to limit or validate object structure:
2309
650
 
2310
651
  ```ruby
2311
- class Person
652
+ class StrictObject
2312
653
  include EasyTalk::Model
654
+
2313
655
  define_schema do
2314
- property :address, Address, ref: true, description: "Primary address", title: "Main Address"
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
- Person.json_schema["properties"]["address"]
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
- ### Interaction with `compose`
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 Employee
675
+ class DynamicConfig
2332
676
  include EasyTalk::Model
677
+
2333
678
  define_schema do
2334
- compose T::AllOf[Person, EmployeeDetails]
2335
- property :badge_number, String
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
- If you also have properties using `$ref`, both the composed models and property models will appear in `$defs`.
2341
-
2342
- ### Best Practices
2343
-
2344
- 1. **Use global configuration for consistency**: If you prefer `$ref` style, enable it globally rather than per-property.
2345
-
2346
- 2. **Consider schema consumers**: Some JSON Schema validators and tools work better with inlined schemas, while others prefer `$ref`. Choose based on your use case.
689
+ **Dependent required:**
2347
690
 
2348
- 3. **Use `$ref` for frequently reused models**: If a model appears in many places, `$ref` reduces schema size and improves maintainability.
2349
-
2350
- 4. **Keep inline for simple, single-use models**: For models used only once, inlining may be more readable.
2351
-
2352
- ### Default Behavior
691
+ ```ruby
692
+ class ShippingInfo
693
+ include EasyTalk::Model
2353
694
 
2354
- By default, `use_refs` is set to `false`, meaning nested models are inlined. This maintains backward compatibility with previous versions of EasyTalk.
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
- ## JSON Schema Compatibility
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
- ### Supported Versions
2359
- EasyTalk supports generating schemas compatible with JSON Schema Draft-04 through Draft 2020-12. Use the `schema_version` configuration option to declare which version your schemas conform to (see [JSON Schema Version](#json-schema-version-schema-keyword) above).
708
+ ### Custom type builders
2360
709
 
2361
- While EasyTalk allows you to specify any draft version via the `$schema` keyword, the generated schema structure is generally compatible across versions. Some newer draft features may require manual adjustment.
710
+ Register custom types with their own schema builders:
2362
711
 
2363
- ### Specification Compliance
2364
- To learn about current capabilities, see the [spec/easy_talk/examples](https://github.com/sergiobayona/easy_talk/tree/main/spec/easy_talk/examples) folder. The examples illustrate how EasyTalk generates JSON Schema in different scenarios.
712
+ ```ruby
713
+ EasyTalk.configure do |config|
714
+ config.register_type(Money, MoneySchemaBuilder)
715
+ end
2365
716
 
2366
- ### Known Limitations
2367
- - Limited support for custom formats
2368
- - Some draft-specific keywords may not be supported
2369
- - Complex composition scenarios may require manual adjustment
717
+ # Or directly:
718
+ EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
719
+ ```
2370
720
 
2371
- ## API Reference
721
+ See the [Custom Type Builders documentation](https://rubydoc.info/gems/easy_talk/EasyTalk/Builders/Registry) for details on creating builders.
2372
722
 
2373
- For complete API documentation, visit [RubyDoc.info](https://rubydoc.info/gems/easy_talk).
723
+ ---
2374
724
 
2375
- ### Core Modules
725
+ ## Known limitations
2376
726
 
2377
- | Module | Description | Source |
2378
- |--------|-------------|--------|
2379
- | [`EasyTalk::Model`](https://rubydoc.info/gems/easy_talk/EasyTalk/Model) | Main mixin providing schema definition, validation, and JSON Schema generation | [lib/easy_talk/model.rb](lib/easy_talk/model.rb) |
2380
- | [`EasyTalk::Schema`](https://rubydoc.info/gems/easy_talk/EasyTalk/Schema) | Schema-only mode without ActiveModel validations | [lib/easy_talk/schema.rb](lib/easy_talk/schema.rb) |
2381
- | [`EasyTalk::SchemaDefinition`](https://rubydoc.info/gems/easy_talk/EasyTalk/SchemaDefinition) | DSL for defining properties, titles, and descriptions | [lib/easy_talk/schema_definition.rb](lib/easy_talk/schema_definition.rb) |
2382
- | [`EasyTalk::Property`](https://rubydoc.info/gems/easy_talk/EasyTalk/Property) | Property definition and type dispatching | [lib/easy_talk/property.rb](lib/easy_talk/property.rb) |
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
- ### Builders
732
+ ---
2385
733
 
2386
- Type-specific builders that convert Ruby definitions to JSON Schema. See the [builders directory](lib/easy_talk/builders/) for all available builders.
734
+ ## Contributing
2387
735
 
2388
- | Builder | Description |
2389
- |---------|-------------|
2390
- | [`ObjectBuilder`](lib/easy_talk/builders/object_builder.rb) | Handles EasyTalk::Model classes |
2391
- | [`StringBuilder`](lib/easy_talk/builders/string_builder.rb) | String type with format, pattern, length constraints |
2392
- | [`IntegerBuilder`](lib/easy_talk/builders/integer_builder.rb) | Integer type with min/max constraints |
2393
- | [`NumberBuilder`](lib/easy_talk/builders/number_builder.rb) | Float/Number type with numeric constraints |
2394
- | [`BooleanBuilder`](lib/easy_talk/builders/boolean_builder.rb) | Boolean type (T::Boolean) |
2395
- | [`TypedArrayBuilder`](lib/easy_talk/builders/typed_array_builder.rb) | Typed arrays (T::Array[Type]) |
2396
- | [`CompositionBuilder`](lib/easy_talk/builders/composition_builder.rb) | Composition types (T::AnyOf, T::OneOf, T::AllOf) |
2397
- | [`UnionBuilder`](lib/easy_talk/builders/union_builder.rb) | Nilable types (T.nilable) |
2398
- | [`TemporalBuilder`](lib/easy_talk/builders/temporal_builder.rb) | Date and DateTime types |
2399
- | [`BaseBuilder`](lib/easy_talk/builders/base_builder.rb) | Base class for all builders |
2400
- | [`Registry`](lib/easy_talk/builders/registry.rb) | Type-to-builder registration |
736
+ - Run `bin/setup`
737
+ - Run specs: `bundle exec rake spec`
738
+ - Run lint: `bundle exec rubocop`
2401
739
 
2402
- ### Configuration & Utilities
740
+ Bug reports and PRs welcome.
2403
741
 
2404
- | Class/Module | Description | Source |
2405
- |--------------|-------------|--------|
2406
- | [`EasyTalk::Configuration`](https://rubydoc.info/gems/easy_talk/EasyTalk/Configuration) | Global configuration options | [lib/easy_talk/configuration.rb](lib/easy_talk/configuration.rb) |
2407
- | [`EasyTalk::ValidationBuilder`](https://rubydoc.info/gems/easy_talk/EasyTalk/ValidationBuilder) | Generates ActiveModel validations from constraints | [lib/easy_talk/validation_builder.rb](lib/easy_talk/validation_builder.rb) |
2408
- | [`EasyTalk::ErrorFormatter`](https://rubydoc.info/gems/easy_talk/EasyTalk/ErrorFormatter) | Formats validation errors (flat, JSON Pointer, RFC 7807, JSON:API) | [lib/easy_talk/error_formatter.rb](lib/easy_talk/error_formatter.rb) |
2409
- | [`EasyTalk::TypeIntrospection`](https://rubydoc.info/gems/easy_talk/EasyTalk/TypeIntrospection) | Type detection utilities | [lib/easy_talk/type_introspection.rb](lib/easy_talk/type_introspection.rb) |
2410
- | [`EasyTalk::Tools::FunctionBuilder`](https://rubydoc.info/gems/easy_talk/EasyTalk/Tools/FunctionBuilder) | LLM function specification generator | [lib/easy_talk/tools/function_builder.rb](lib/easy_talk/tools/function_builder.rb) |
742
+ ---
2411
743
 
2412
744
  ## License
2413
745
 
2414
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
746
+ MIT