easy_talk 3.1.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -39
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +164 -0
  5. data/README.md +442 -1529
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +169 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/primitive-schema-rfc.md +894 -0
  14. data/docs/property-types.markdown +212 -0
  15. data/docs/schema-definition.markdown +180 -0
  16. data/lib/easy_talk/builders/base_builder.rb +6 -3
  17. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  18. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  19. data/lib/easy_talk/builders/composition_builder.rb +16 -13
  20. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  21. data/lib/easy_talk/builders/null_builder.rb +4 -1
  22. data/lib/easy_talk/builders/number_builder.rb +4 -1
  23. data/lib/easy_talk/builders/object_builder.rb +109 -33
  24. data/lib/easy_talk/builders/registry.rb +182 -0
  25. data/lib/easy_talk/builders/string_builder.rb +3 -1
  26. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  27. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  28. data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
  29. data/lib/easy_talk/builders/union_builder.rb +5 -1
  30. data/lib/easy_talk/configuration.rb +47 -2
  31. data/lib/easy_talk/error_formatter/base.rb +100 -0
  32. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  33. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  34. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  35. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  36. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  37. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  38. data/lib/easy_talk/error_formatter.rb +143 -0
  39. data/lib/easy_talk/errors.rb +3 -0
  40. data/lib/easy_talk/errors_helper.rb +66 -34
  41. data/lib/easy_talk/json_schema_equality.rb +46 -0
  42. data/lib/easy_talk/keywords.rb +0 -1
  43. data/lib/easy_talk/model.rb +148 -89
  44. data/lib/easy_talk/model_helper.rb +17 -0
  45. data/lib/easy_talk/naming_strategies.rb +24 -0
  46. data/lib/easy_talk/property.rb +23 -94
  47. data/lib/easy_talk/ref_helper.rb +33 -0
  48. data/lib/easy_talk/schema.rb +199 -0
  49. data/lib/easy_talk/schema_definition.rb +57 -5
  50. data/lib/easy_talk/schema_methods.rb +111 -0
  51. data/lib/easy_talk/sorbet_extension.rb +1 -0
  52. data/lib/easy_talk/tools/function_builder.rb +1 -1
  53. data/lib/easy_talk/type_introspection.rb +222 -0
  54. data/lib/easy_talk/types/base_composer.rb +2 -1
  55. data/lib/easy_talk/types/composer.rb +4 -0
  56. data/lib/easy_talk/types/tuple.rb +77 -0
  57. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
  58. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  59. data/lib/easy_talk/validation_adapters/base.rb +156 -0
  60. data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
  61. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  62. data/lib/easy_talk/validation_builder.rb +29 -309
  63. data/lib/easy_talk/version.rb +1 -1
  64. data/lib/easy_talk.rb +42 -0
  65. metadata +38 -7
  66. data/docs/404.html +0 -25
  67. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  68. data/easy_talk.gemspec +0 -39
data/README.md CHANGED
@@ -2,1832 +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)
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)
5
11
 
6
- ## Introduction
7
-
8
- ### What is EasyTalk?
9
- EasyTalk is a Ruby library that simplifies defining and generating JSON Schema. It provides an intuitive interface for Ruby developers to define structured data models that can be used for validation and documentation.
10
-
11
- ### Key Features
12
- * **Intuitive Schema Definition**: Use Ruby classes and methods to define JSON Schema documents easily.
13
- * **Automatic ActiveModel Validations**: Schema constraints automatically generate corresponding ActiveModel validations (configurable).
14
- * **Works for plain Ruby classes and ActiveModel classes**: Integrate with existing code or build from scratch.
15
- * **LLM Function Support**: Ideal for integrating with Large Language Models (LLMs) such as OpenAI's GPT series. EasyTalk enables you to effortlessly create JSON Schema documents describing the inputs and outputs of LLM function calls.
16
- * **Schema Composition**: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
17
- * **Enhanced Model Integration**: Automatic instantiation of nested EasyTalk models from hash attributes.
18
- * **Flexible Configuration**: Global and per-model configuration options for fine-tuned control.
19
- * **JSON Schema Version Support**: Configure the `$schema` keyword to declare which JSON Schema draft version your schemas conform to (Draft-04 through Draft 2020-12).
20
- * **Schema Identification**: Configure the `$id` keyword to provide a unique identifier URI for your schemas.
21
- * **Schema References**: Use `$ref` and `$defs` for reusable schema definitions, reducing duplication when models are used in multiple places.
22
-
23
- ### Use Cases
24
- - API request/response validation
25
- - LLM function definitions
26
- - Object structure documentation
27
- - Data validation and transformation
28
- - Configuration schema definitions
29
-
30
- ### Inspiration
31
- Inspired by Python's Pydantic 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.
32
13
 
33
- ## Installation
14
+ Think “Pydantic-style ergonomics” for Ruby, with first-class JSON Schema output.
34
15
 
35
- ### Requirements
36
- - Ruby 3.2 or higher
16
+ ---
37
17
 
38
- ### Version 2.0.0 Breaking Changes
18
+ ## Why EasyTalk?
39
19
 
40
- ⚠️ **IMPORTANT**: Version 2.0.0 includes breaking changes. Please review before upgrading:
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.
41
21
 
42
- - **Removed**: Block-style nested object definitions (using `Hash do ... end`)
43
- - **Migration**: Use class references instead of inline Hash definitions
22
+ EasyTalk makes the schema definition the single source of truth, so you can:
44
23
 
45
- ```ruby
46
- # No longer supported (v1.x style)
47
- define_schema do
48
- property :address, Hash do
49
- property :street, String
50
- property :city, String
51
- end
52
- end
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
53
28
 
54
- # New approach (v2.x style)
55
- class Address
56
- include EasyTalk::Model
57
- define_schema do
58
- property :street, String
59
- property :city, String
60
- end
61
- end
29
+ - **Stop arguing with JSON Schema’s verbosity**
30
+ Express constraints in Ruby where you already live:
31
+ ```ruby
32
+ property :email, String, format: "email"
33
+ property :age, Integer, minimum: 18
34
+ property :tags, T::Array[String], min_items: 1
35
+ ```
62
36
 
63
- class User
64
- include EasyTalk::Model
65
- define_schema do
66
- property :address, Address # Reference the class directly
67
- end
68
- end
69
- ```
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
70
44
 
71
- ### Installation Steps
72
- Add EasyTalk to your application's Gemfile:
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.
73
47
 
74
- ```ruby
75
- gem 'easy_talk'
76
- ```
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
77
54
 
78
- Or install it directly:
55
+ - **LLM tool/function schemas without a second schema layer**
56
+ Use the same contract to generate JSON Schema for function/tool calling.
79
57
 
80
- ```bash
81
- $ gem install easy_talk
82
- ```
58
+ EasyTalk is for teams who want their data contracts to be **correct, reusable, and boring** (the good kind of boring).
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ### Requirements
65
+ - Ruby **3.2+**
83
66
 
84
- ### Verification
85
- After installation, you can verify it's working by creating a simple model:
67
+ Add to your Gemfile:
86
68
 
87
69
  ```ruby
88
- require 'easy_talk'
70
+ gem "easy_talk"
71
+ ```
89
72
 
90
- class Test
91
- include EasyTalk::Model
92
-
93
- define_schema do
94
- property :name, String
95
- end
96
- end
73
+ Then:
97
74
 
98
- puts Test.json_schema
75
+ ```bash
76
+ bundle install
99
77
  ```
100
78
 
101
- ## Quick Start
79
+ ---
102
80
 
103
- ### Minimal Example
104
- Here's a basic example to get you started with EasyTalk:
81
+ ## Quick start
105
82
 
106
83
  ```ruby
84
+ require "easy_talk"
85
+
107
86
  class User
108
87
  include EasyTalk::Model
109
88
 
110
89
  define_schema do
111
90
  title "User"
112
91
  description "A user of the system"
113
- property :name, String, description: "The user's name"
92
+
93
+ property :id, String
94
+ property :name, String, min_length: 2
114
95
  property :email, String, format: "email"
115
96
  property :age, Integer, minimum: 18
116
97
  end
117
98
  end
99
+
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
118
104
  ```
119
105
 
120
- ### Generated JSON Schema
121
- Calling `User.json_schema` will generate:
106
+ **Generated JSON Schema:**
122
107
 
123
- ```ruby
108
+ ```json
124
109
  {
125
- "type" => "object",
126
- "title" => "User",
127
- "description" => "A user of the system",
128
- "properties" => {
129
- "name" => {
130
- "type" => "string",
131
- "description" => "The user's name"
132
- },
133
- "email" => {
134
- "type" => "string",
135
- "format" => "email"
136
- },
137
- "age" => {
138
- "type" => "integer",
139
- "minimum" => 18
140
- }
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 }
141
118
  },
142
- "required" => ["name", "email", "age"]
119
+ "required": ["id", "name", "email", "age"]
143
120
  }
144
121
  ```
145
122
 
146
- ### Basic Usage
147
- Creating and validating an instance of your model:
148
-
149
- ```ruby
150
- user = User.new(name: "John Doe", email: "john@example.com", age: 25)
151
- user.valid? # => true (automatically validates based on schema constraints)
123
+ ---
152
124
 
153
- user.age = 17
154
- user.valid? # => false (violates minimum: 18 constraint)
155
- user.errors[:age] # => ["must be greater than or equal to 18"]
156
- ```
125
+ ## Property constraints
157
126
 
158
- ## Core Concepts
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"` |
159
141
 
160
- ### Schema Definition
161
- 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.
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
162
146
 
163
- ```ruby
164
- class MyModel
165
- include EasyTalk::Model
147
+ When `auto_validations` is enabled (default), these constraints automatically generate corresponding ActiveModel validations.
166
148
 
167
- define_schema do
168
- title "My Model"
169
- description "Description of my model"
170
- property :some_property, String
171
- property :another_property, Integer
172
- end
173
- end
174
- ```
149
+ ---
175
150
 
176
- ### Property Types
151
+ ## Core concepts
177
152
 
178
- #### Ruby Types
179
- EasyTalk supports standard Ruby types directly:
153
+ ### Required vs optional vs nullable (don't get tricked)
180
154
 
181
- - `String`: String values
182
- - `Integer`: Integer values
183
- - `Float`: Floating-point numbers
184
- - `Date`: Date values
185
- - `DateTime`: Date and time values
155
+ JSON Schema distinguishes:
156
+ - **Optional**: property may be omitted (not in `required`)
157
+ - **Nullable**: property may be `null` (type includes `"null"`)
186
158
 
187
- #### Sorbet-Style Types
188
- For complex types, EasyTalk uses Sorbet-style type notation:
159
+ EasyTalk mirrors that precisely:
189
160
 
190
- - `T::Boolean`: Boolean values (true/false)
191
- - `T::Array[Type]`: Arrays with items of a specific type
192
- - `T.nilable(Type)`: Type that can also be nil
161
+ ```ruby
162
+ class Profile
163
+ include EasyTalk::Model
193
164
 
194
- #### Custom Types
195
- EasyTalk supports special composition types:
165
+ define_schema do
166
+ # required, not nullable
167
+ property :name, String
196
168
 
197
- - `T::AnyOf[Type1, Type2, ...]`: Value can match any of the specified schemas
198
- - `T::OneOf[Type1, Type2, ...]`: Value must match exactly one of the specified schemas
199
- - `T::AllOf[Type1, Type2, ...]`: Value must match all of the specified schemas
169
+ # required, nullable (must exist, may be null)
170
+ property :age, T.nilable(Integer)
200
171
 
201
- ### Property Constraints
202
- Property constraints depend on the type of property. Some common constraints include:
172
+ # optional, not nullable (may be omitted, but cannot be null if present)
173
+ property :nickname, String, optional: true
203
174
 
204
- - `description`: A description of the property
205
- - `title`: A title for the property
206
- - `format`: A format hint for the property (e.g., "email", "date")
207
- - `enum`: A list of allowed values
208
- - `minimum`/`maximum`: Minimum/maximum values for numbers
209
- - `min_length`/`max_length`: Minimum/maximum length for strings
210
- - `pattern`: A regular expression pattern for strings
211
- - `min_items`/`max_items`: Minimum/maximum number of items for arrays
212
- - `unique_items`: Whether array items must be unique
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
+ ```
213
182
 
214
- ### Required vs Optional Properties
215
- By default, all properties defined in an EasyTalk model are required. You can make a property optional by specifying `optional: true`:
183
+ By default, `T.nilable(Type)` makes a field **nullable but still required**.
184
+ If you want “nilable implies optional behavior globally:
216
185
 
217
186
  ```ruby
218
- define_schema do
219
- property :name, String
220
- property :middle_name, String, optional: true
187
+ EasyTalk.configure do |config|
188
+ config.nilable_is_optional = true
221
189
  end
222
190
  ```
223
191
 
224
- In this example, `name` is required but `middle_name` is optional.
192
+ ---
225
193
 
226
- ### Automatic Validation Generation
227
- EasyTalk automatically generates ActiveModel validations from your schema constraints. This feature is enabled by default but can be configured:
194
+ ### Nested models (and automatic instantiation)
195
+
196
+ Define nested objects as separate classes, then reference them:
228
197
 
229
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
+
230
208
  class User
231
209
  include EasyTalk::Model
232
-
210
+
233
211
  define_schema do
234
- property :name, String, min_length: 2, max_length: 50
235
- property :email, String, format: "email"
236
- property :age, Integer, minimum: 18, maximum: 120
237
- property :status, String, enum: ["active", "inactive", "pending"]
212
+ property :name, String
213
+ property :address, Address
238
214
  end
239
- # Validations are automatically generated:
240
- # validates :name, presence: true, length: { minimum: 2, maximum: 50 }
241
- # validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
242
- # validates :age, presence: true, numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 120 }
243
- # validates :status, presence: true, inclusion: { in: ["active", "inactive", "pending"] }
244
215
  end
245
216
 
246
- user = User.new(name: "Jo", email: "invalid-email", age: 17)
247
- user.valid? # => false
248
- user.errors.full_messages
249
- # => ["Name is too short (minimum is 2 characters)",
250
- # "Email is invalid",
251
- # "Age must be greater than or equal to 18"]
217
+ user = User.new(
218
+ name: "John",
219
+ address: { street: "123 Main St", city: "Boston" } # Hash becomes Address automatically
220
+ )
221
+
222
+ user.address.class # => Address
252
223
  ```
253
224
 
254
- ### Manual Validation Overrides
255
- You can still add manual validations alongside automatic ones:
225
+ Nested models inside arrays work too:
256
226
 
257
227
  ```ruby
258
- class User
228
+ class Order
259
229
  include EasyTalk::Model
260
-
261
- # Custom validation in addition to automatic ones
262
- validates :email, uniqueness: true
263
- validate :complex_business_rule
264
-
230
+
265
231
  define_schema do
266
- property :name, String
267
- property :email, String, format: "email"
268
- property :age, Integer, minimum: 18
269
- end
270
-
271
- private
272
-
273
- def complex_business_rule
274
- # Custom validation logic
232
+ property :line_items, T::Array[Address], min_items: 1
275
233
  end
276
234
  end
277
235
  ```
278
236
 
279
- ## Defining Schemas
237
+ ---
280
238
 
281
- ### Basic Schema Structure
282
- A schema definition consists of a class that includes `EasyTalk::Model` and a `define_schema` block:
239
+ ### Tuple arrays (fixed-position types)
240
+
241
+ Use `T::Tuple` for arrays where each position has a specific type (e.g., coordinates, CSV rows, database records):
283
242
 
284
243
  ```ruby
285
- class Person
244
+ class GeoLocation
286
245
  include EasyTalk::Model
287
246
 
288
247
  define_schema do
289
- title "Person"
290
248
  property :name, String
291
- property :age, Integer
249
+ # Fixed: [latitude, longitude]
250
+ property :coordinates, T::Tuple[Float, Float]
292
251
  end
293
252
  end
294
- ```
295
-
296
- ### Property Definitions
297
- Properties are defined using the `property` method, which takes a name, a type, and optional constraints:
298
253
 
299
- ```ruby
300
- property :name, String, description: "The person's name", title: "Full Name"
301
- property :age, Integer, minimum: 0, maximum: 120, description: "The person's age"
254
+ location = GeoLocation.new(
255
+ name: 'Office',
256
+ coordinates: [40.7128, -74.0060]
257
+ )
302
258
  ```
303
259
 
304
- ### Arrays and Collections
305
- Arrays can be defined using the `T::Array` type:
260
+ **Generated JSON Schema:**
306
261
 
307
- ```ruby
308
- property :tags, T::Array[String], min_items: 1, unique_items: true
309
- property :scores, T::Array[Integer], description: "List of scores"
262
+ ```json
263
+ {
264
+ "properties": {
265
+ "coordinates": {
266
+ "type": "array",
267
+ "items": [
268
+ { "type": "number" },
269
+ { "type": "number" }
270
+ ]
271
+ }
272
+ }
273
+ }
310
274
  ```
311
275
 
312
- You can also define arrays of complex types:
276
+ **Mixed-type tuples:**
313
277
 
314
278
  ```ruby
315
- property :addresses, T::Array[Address], description: "List of addresses"
316
- ```
317
-
318
- ### Constraints and Automatic Validations
319
- Constraints are added to properties and are used for both schema generation and automatic validation generation:
279
+ class DataRow
280
+ include EasyTalk::Model
320
281
 
321
- ```ruby
322
- define_schema do
323
- property :name, String, min_length: 2, max_length: 50
324
- property :email, String, format: "email"
325
- property :category, String, enum: ["A", "B", "C"]
326
- property :score, Float, minimum: 0.0, maximum: 100.0
327
- property :tags, T::Array[String], min_items: 1, max_items: 10
282
+ define_schema do
283
+ # Fixed: [name, age, active]
284
+ property :row, T::Tuple[String, Integer, T::Boolean]
285
+ end
328
286
  end
329
- # Automatically generates equivalent ActiveModel validations:
330
- # validates :name, presence: true, length: { minimum: 2, maximum: 50 }
331
- # validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
332
- # validates :category, presence: true, inclusion: { in: ["A", "B", "C"] }
333
- # validates :score, presence: true, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 }
334
- # validates :tags, presence: true, length: { minimum: 1, maximum: 10 }
335
287
  ```
336
288
 
337
- ### Supported Constraint-to-Validation Mappings
338
-
339
- | Constraint | Validation Generated |
340
- |------------|---------------------|
341
- | `min_length`, `max_length` | `length: { minimum: X, maximum: Y }` |
342
- | `minimum`, `maximum` | `numericality: { greater_than_or_equal_to: X, less_than_or_equal_to: Y }` |
343
- | `format: "email"` | `format: { with: URI::MailTo::EMAIL_REGEXP }` |
344
- | `format: "url"` or `format: "uri"` | `format: { with: URI::regexp }` |
345
- | `pattern: /regex/` | `format: { with: /regex/ }` |
346
- | `enum: [...]` | `inclusion: { in: [...] }` |
347
- | `min_items`, `max_items` (arrays) | `length: { minimum: X, maximum: Y }` |
348
- | `optional: true` | Skips presence validation |
349
- | `T.nilable(Type)` | Allows nil values, skips presence validation |
350
-
351
- ### Additional Properties
352
- 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:
289
+ **Controlling extra items:**
353
290
 
354
291
  ```ruby
355
292
  define_schema do
356
- property :name, String
357
- additional_properties true
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]
358
301
  end
359
302
  ```
360
303
 
361
- With `additional_properties true`, you can add arbitrary properties to your model instances:
304
+ **Tuple validation:**
362
305
 
363
306
  ```ruby
364
- company = Company.new
365
- company.name = "Acme Corp" # Defined property
366
- company.location = "New York" # Additional property
367
- company.employee_count = 100 # Additional property
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"]
368
311
  ```
369
312
 
370
- ## Schema Composition
313
+ ---
371
314
 
372
- ### Using T::AnyOf
373
- The `T::AnyOf` type allows a property to match any of the specified schemas:
315
+ ### Composition (AnyOf / OneOf / AllOf)
374
316
 
375
317
  ```ruby
376
- class Payment
318
+ class ProductA
377
319
  include EasyTalk::Model
378
-
379
320
  define_schema do
380
- property :details, T::AnyOf[CreditCard, Paypal, BankTransfer]
321
+ property :sku, String
322
+ property :weight, Float
381
323
  end
382
324
  end
383
- ```
384
325
 
385
- ### Using T::OneOf
386
- The `T::OneOf` type requires a property to match exactly one of the specified schemas:
326
+ class ProductB
327
+ include EasyTalk::Model
328
+ define_schema do
329
+ property :sku, String
330
+ property :color, String
331
+ end
332
+ end
387
333
 
388
- ```ruby
389
- class Contact
334
+ class Cart
390
335
  include EasyTalk::Model
391
336
 
392
337
  define_schema do
393
- property :contact, T::OneOf[PhoneContact, EmailContact]
338
+ property :items, T::Array[T::AnyOf[ProductA, ProductB]]
394
339
  end
395
340
  end
396
341
  ```
397
342
 
398
- ### Using T::AllOf
399
- The `T::AllOf` type requires a property to match all of the specified schemas:
343
+ ---
344
+
345
+ ## Validations
346
+
347
+ ### Automatic validations (default)
348
+
349
+ EasyTalk can generate ActiveModel validations from constraints:
400
350
 
401
351
  ```ruby
402
- class VehicleRegistration
403
- include EasyTalk::Model
404
-
405
- define_schema do
406
- compose T::AllOf[VehicleIdentification, OwnerInfo, RegistrationDetails]
407
- end
352
+ EasyTalk.configure do |config|
353
+ config.auto_validations = true
408
354
  end
409
355
  ```
410
356
 
411
- ### Complex Compositions
412
- You can combine composition types to create complex schemas:
357
+ Disable globally:
413
358
 
414
359
  ```ruby
415
- class ComplexObject
416
- include EasyTalk::Model
417
-
418
- define_schema do
419
- property :basic_info, BaseInfo
420
- property :specific_details, T::OneOf[DetailTypeA, DetailTypeB]
421
- property :metadata, T::AnyOf[AdminMetadata, UserMetadata, nil]
422
- end
360
+ EasyTalk.configure do |config|
361
+ config.auto_validations = false
423
362
  end
424
363
  ```
425
364
 
426
- ### Reusing Models
427
- Models can reference other models to create hierarchical schemas:
365
+ When auto validations are off, you can still write validations manually:
428
366
 
429
367
  ```ruby
430
- class Address
368
+ class User
431
369
  include EasyTalk::Model
432
-
370
+
371
+ validates :name, presence: true, length: { minimum: 2 }
372
+
433
373
  define_schema do
434
- property :street, String
435
- property :city, String
436
- property :state, String
437
- property :zip, String
374
+ property :name, String, min_length: 2
438
375
  end
439
376
  end
377
+ ```
440
378
 
441
- class User
379
+ ### Per-model validation control
380
+
381
+ ```ruby
382
+ class LegacyModel
442
383
  include EasyTalk::Model
443
-
444
- define_schema do
445
- property :name, String
446
- property :address, Address
384
+
385
+ define_schema(validations: false) do
386
+ property :data, String, min_length: 1 # no validation generated
447
387
  end
448
388
  end
449
389
  ```
450
390
 
451
- ## ActiveModel Integration
452
-
453
- ### Enhanced Validation System
454
- EasyTalk models include comprehensive ActiveModel validation support with automatic generation:
391
+ ### Per-property validation control
455
392
 
456
393
  ```ruby
457
394
  class User
458
395
  include EasyTalk::Model
459
-
460
- # Manual validations work alongside automatic ones
461
- validates :age, comparison: { greater_than: 21 } # Additional business rule
462
- validates :height, numericality: { greater_than: 0 } # Overrides auto-validation
463
-
396
+
464
397
  define_schema do
465
- property :name, String, min_length: 2 # Auto-generates presence + length validations
466
- property :age, Integer, minimum: 18 # Auto-generates presence + numericality validations
467
- property :height, Float # Auto-generates presence validation (overridden above)
398
+ property :name, String, min_length: 2
399
+ property :legacy_field, String, validate: false
468
400
  end
469
401
  end
470
402
  ```
471
403
 
472
- ### Error Handling
473
- You can access validation errors using the standard ActiveModel methods:
404
+ ### Validation adapters
405
+
406
+ EasyTalk uses a pluggable adapter system:
474
407
 
475
408
  ```ruby
476
- user = User.new(name: "J", age: 18, height: -5.9)
477
- user.valid? # => false
478
- user.errors[:name] # => ["is too short (minimum is 2 characters)"]
479
- user.errors[:age] # => ["must be greater than 21"] # Custom validation
480
- user.errors[:height] # => ["must be greater than 0"] # Overridden validation
409
+ EasyTalk.configure do |config|
410
+ config.validation_adapter = :active_model # default
411
+ # config.validation_adapter = :none # disable validation generation
412
+ end
481
413
  ```
482
414
 
483
- ### Model Attributes
484
- EasyTalk models provide getters and setters for all defined properties:
415
+ ---
416
+
417
+ ## Error formatting
418
+
419
+ Instance helpers:
485
420
 
486
421
  ```ruby
487
- user = User.new
488
- user.name = "John"
489
- user.age = 30
490
- puts user.name # => "John"
422
+ user.validation_errors_flat
423
+ user.validation_errors_json_pointer
424
+ user.validation_errors_rfc7807
425
+ user.validation_errors_jsonapi
491
426
  ```
492
427
 
493
- You can also initialize a model with a hash of attributes, including nested EasyTalk models:
428
+ Format directly:
494
429
 
495
430
  ```ruby
496
- user = User.new(name: "John", age: 30, height: 5.9)
431
+ EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: "User Validation Failed")
432
+ ```
497
433
 
498
- # NEW in v2.0.0: Automatic nested model instantiation
499
- class Address
500
- include EasyTalk::Model
501
- define_schema do
502
- property :street, String
503
- property :city, String
504
- end
505
- end
434
+ Global defaults:
506
435
 
507
- class User
508
- include EasyTalk::Model
509
- define_schema do
510
- property :name, String
511
- property :address, Address
512
- end
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
513
441
  end
514
-
515
- # Hash attributes automatically instantiate nested models
516
- user = User.new(
517
- name: "John",
518
- address: { street: "123 Main St", city: "Boston" }
519
- )
520
- user.address.class # => Address (automatically instantiated)
521
- user.address.street # => "123 Main St"
522
442
  ```
523
443
 
524
- ## Advanced Features
444
+ ---
445
+
446
+ ## Schema-only mode
525
447
 
526
- ### LLM Function Generation
527
- EasyTalk provides a helper method for generating OpenAI function specifications:
448
+ If you want schema generation and attribute accessors **without** ActiveModel validation:
528
449
 
529
450
  ```ruby
530
- class Weather
531
- include EasyTalk::Model
532
-
451
+ class ApiContract
452
+ include EasyTalk::Schema
453
+
533
454
  define_schema do
534
- title "GetWeather"
535
- description "Get the current weather in a given location"
536
- property :location, String, description: "The city and state, e.g. San Francisco, CA"
537
- property :unit, String, enum: ["celsius", "fahrenheit"], default: "fahrenheit"
455
+ title "API Contract"
456
+ property :name, String, min_length: 2
457
+ property :age, Integer, minimum: 0
538
458
  end
539
459
  end
540
460
 
541
- function_spec = EasyTalk::Tools::FunctionBuilder.new(Weather)
461
+ ApiContract.json_schema
462
+ contract = ApiContract.new(name: "Test", age: 25)
463
+
464
+ # No validations available:
465
+ # contract.valid? # => NoMethodError
542
466
  ```
543
467
 
544
- This generates a function specification compatible with OpenAI's function calling API.
468
+ Use this for documentation, OpenAPI generation, or when validation happens elsewhere.
545
469
 
546
- ### Schema Transformation
547
- You can transform EasyTalk schemas into various formats:
470
+ ---
471
+
472
+ ## Configuration highlights
548
473
 
549
474
  ```ruby
550
- # Get Ruby hash representation
551
- schema_hash = User.schema
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
552
486
 
553
- # Get JSON Schema representation
554
- json_schema = User.json_schema
487
+ # Validations
488
+ config.auto_validations = true
489
+ config.validation_adapter = :active_model
555
490
 
556
- # Convert to JSON string
557
- json_string = User.json_schema.to_json
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
558
496
  ```
559
497
 
560
- ### Type Checking and Validation
561
- EasyTalk performs basic type checking during schema definition:
562
-
563
- ```ruby
564
- # This will raise an error because "minimum" should be used with numeric types
565
- property :name, String, minimum: 1 # Error!
566
-
567
- # This will raise an error because enum values must match the property type
568
- property :age, Integer, enum: ["young", "old"] # Error!
569
- ```
498
+ ---
570
499
 
571
- ### Custom Type Builders
572
- For advanced use cases, you can create custom type builders:
500
+ ## Advanced topics
573
501
 
574
- ```ruby
575
- module EasyTalk
576
- module Builders
577
- class MyCustomTypeBuilder < BaseBuilder
578
- # Custom implementation
579
- end
580
- end
581
- end
582
- ```
502
+ For more detailed documentation, see the [full API reference on RubyDoc](https://rubydoc.info/gems/easy_talk).
583
503
 
584
- ## Configuration
504
+ ### JSON Schema drafts, `$id`, and `$ref`
585
505
 
586
- ### Global Settings
587
- You can configure EasyTalk globally:
506
+ EasyTalk can emit `$schema` for multiple drafts (Draft-04 through 2020-12), supports `$id`, and can use `$ref`/`$defs` for reusable definitions:
588
507
 
589
508
  ```ruby
590
509
  EasyTalk.configure do |config|
591
- # Schema behavior options
592
- config.default_additional_properties = false # Control additional properties on all models
593
- config.nilable_is_optional = false # Makes T.nilable properties also optional
594
- config.auto_validations = true # Automatically generate ActiveModel validations
595
- config.schema_version = :none # JSON Schema version for $schema keyword
596
- # Options: :none, :draft202012, :draft201909, :draft7, :draft6, :draft4
597
- config.schema_id = nil # Base URI for $id keyword (nil = no $id)
598
- config.use_refs = false # Use $ref for nested models instead of inlining
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
599
513
  end
600
514
  ```
601
515
 
602
- ### Automatic Validation Configuration
603
- The new `auto_validations` option (enabled by default) automatically generates ActiveModel validations from your schema constraints:
516
+ #### External schema references
517
+
518
+ Use external URIs in `$ref` for modular, reusable schemas:
604
519
 
605
520
  ```ruby
606
- # Disable automatic validations globally
607
521
  EasyTalk.configure do |config|
608
- config.auto_validations = false
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
609
526
  end
610
527
 
611
- # Now you must manually define validations
612
- class User
528
+ class Address
613
529
  include EasyTalk::Model
614
-
615
- validates :name, presence: true, length: { minimum: 2 }
616
- validates :age, presence: true, numericality: { greater_than_or_equal_to: 18 }
617
-
530
+
618
531
  define_schema do
619
- property :name, String, min_length: 2
620
- property :age, Integer, minimum: 18
532
+ property :street, String
533
+ property :city, String
621
534
  end
622
535
  end
623
- ```
624
536
 
625
- ### Per-Model Configuration
626
- You can configure additional properties for individual models:
627
-
628
- ```ruby
629
- class User
537
+ class Customer
630
538
  include EasyTalk::Model
631
-
539
+
632
540
  define_schema do
633
- title "User"
634
- additional_properties true # Allow arbitrary additional properties on this model
635
541
  property :name, String
636
- property :email, String, format: "email"
542
+ property :address, Address
637
543
  end
638
544
  end
639
- ```
640
545
 
641
- ## Examples
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
+ # }
559
+ ```
642
560
 
643
- ### User Registration (with Auto-Validations)
561
+ **Explicit schema IDs:**
644
562
 
645
563
  ```ruby
646
- class User
564
+ class Address
647
565
  include EasyTalk::Model
648
566
 
649
- # Additional custom validations beyond automatic ones
650
- validates :email, uniqueness: true
651
- validates :password, confirmation: true
652
-
653
567
  define_schema do
654
- title "User Registration"
655
- description "User registration information"
656
- property :name, String, min_length: 2, max_length: 100, description: "User's full name"
657
- property :email, String, format: "email", description: "User's email address"
658
- property :password, String, min_length: 8, max_length: 128, description: "User's password"
659
- property :notify, T::Boolean, optional: true, description: "Whether to send notifications"
568
+ schema_id 'https://example.com/schemas/address'
569
+ property :street, String
660
570
  end
661
- # Auto-generated validations:
662
- # validates :name, presence: true, length: { minimum: 2, maximum: 100 }
663
- # validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
664
- # validates :password, presence: true, length: { minimum: 8, maximum: 128 }
665
- # validates :notify, inclusion: { in: [true, false] } - only if present (optional: true)
666
571
  end
667
-
668
- # Usage with automatic validation
669
- user = User.new(
670
- name: "John Doe",
671
- email: "john@example.com",
672
- password: "secretpassword123",
673
- notify: true
674
- )
675
- user.valid? # => true (assuming email is unique)
676
-
677
- # Invalid data triggers auto-generated validations
678
- invalid_user = User.new(
679
- name: "J", # Too short
680
- email: "invalid-email", # Invalid format
681
- password: "123" # Too short
682
- )
683
- invalid_user.valid? # => false
684
- invalid_user.errors.full_messages
685
- # => ["Name is too short (minimum is 2 characters)",
686
- # "Email is invalid",
687
- # "Password is too short (minimum is 8 characters)"]
688
572
  ```
689
573
 
690
- ### Payment Processing
574
+ **Per-property ref control:**
691
575
 
692
576
  ```ruby
693
- class CreditCard
577
+ class Customer
694
578
  include EasyTalk::Model
695
579
 
696
580
  define_schema do
697
- property :CardNumber, String
698
- property :CardType, String, enum: %w[Visa MasterCard AmericanExpress]
699
- property :CardExpMonth, Integer, minimum: 1, maximum: 12
700
- property :CardExpYear, Integer, minimum: Date.today.year, maximum: Date.today.year + 10
701
- property :CardCVV, String, pattern: '^[0-9]{3,4}$'
702
- additional_properties false
581
+ property :address, Address, ref: false # Inline instead of ref
582
+ property :billing, Address # Uses ref (global setting)
703
583
  end
704
584
  end
585
+ ```
705
586
 
706
- class Paypal
707
- include EasyTalk::Model
587
+ ### Additional properties with types
708
588
 
709
- define_schema do
710
- property :PaypalEmail, String, format: 'email'
711
- property :PaypalPasswordEncrypted, String
712
- additional_properties false
713
- end
714
- end
589
+ Beyond boolean values, `additional_properties` now supports type constraints for dynamic properties:
715
590
 
716
- class BankTransfer
591
+ ```ruby
592
+ class Config
717
593
  include EasyTalk::Model
718
594
 
719
595
  define_schema do
720
- property :BankName, String
721
- property :AccountNumber, String
722
- property :RoutingNumber, String
723
- property :AccountType, String, enum: %w[Checking Savings]
724
- additional_properties false
596
+ property :name, String
597
+
598
+ # Allow any string-typed additional properties
599
+ additional_properties String
725
600
  end
726
601
  end
727
602
 
728
- class Payment
603
+ config = Config.new(name: 'app')
604
+ config.label = 'Production' # Dynamic property
605
+ config.as_json
606
+ # => { 'name' => 'app', 'label' => 'Production' }
607
+ ```
608
+
609
+ **With constraints:**
610
+
611
+ ```ruby
612
+ class StrictConfig
729
613
  include EasyTalk::Model
730
614
 
731
615
  define_schema do
732
- title 'Payment'
733
- description 'Payment info'
734
- property :PaymentMethod, String, enum: %w[CreditCard Paypal BankTransfer]
735
- property :Details, T::AnyOf[CreditCard, Paypal, BankTransfer]
616
+ property :id, Integer
617
+ # Integer values between 0 and 100 only
618
+ additional_properties Integer, minimum: 0, maximum: 100
736
619
  end
737
620
  end
621
+
622
+ StrictConfig.json_schema
623
+ # =>
624
+ # {
625
+ # "properties": { "id": { "type": "integer" } },
626
+ # "additionalProperties": {
627
+ # "type": "integer",
628
+ # "minimum": 0,
629
+ # "maximum": 100
630
+ # }
631
+ # }
738
632
  ```
739
633
 
740
- ### Complex Object Hierarchies
634
+ **Nested models as additional properties:**
741
635
 
742
636
  ```ruby
743
- class Address
637
+ class Person
744
638
  include EasyTalk::Model
745
639
 
746
640
  define_schema do
747
- property :street, String
748
- property :city, String
749
- property :state, String
750
- property :zip, String, pattern: '^[0-9]{5}(?:-[0-9]{4})?$'
641
+ property :name, String
642
+ additional_properties Address # All additional properties must be Address objects
751
643
  end
752
644
  end
645
+ ```
646
+
647
+ ### Object-level constraints
753
648
 
754
- class Employee
649
+ Apply schema-wide constraints to limit or validate object structure:
650
+
651
+ ```ruby
652
+ class StrictObject
755
653
  include EasyTalk::Model
756
654
 
757
655
  define_schema do
758
- title 'Employee'
759
- description 'Company employee'
760
- property :name, String, title: 'Full Name'
761
- property :gender, String, enum: %w[male female other]
762
- property :department, T.nilable(String)
763
- property :hire_date, Date
764
- property :active, T::Boolean, default: true
765
- property :addresses, T.nilable(T::Array[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
766
665
  end
767
666
  end
768
667
 
769
- class Company
668
+ obj = StrictObject.new(required1: 'a')
669
+ obj.valid? # => false (only 1 property, needs at least 2)
670
+ ```
671
+
672
+ **Pattern properties:**
673
+
674
+ ```ruby
675
+ class DynamicConfig
770
676
  include EasyTalk::Model
771
677
 
772
678
  define_schema do
773
- title 'Company'
774
679
  property :name, String
775
- property :employees, T::Array[Employee], title: 'Company Employees', description: 'A list of company employees'
680
+
681
+ # Properties matching /^env_/ must be strings
682
+ pattern_properties(
683
+ '^env_' => { type: 'string' }
684
+ )
776
685
  end
777
686
  end
778
687
  ```
779
688
 
780
- ### API Integration
689
+ **Dependent required:**
781
690
 
782
691
  ```ruby
783
- # app/controllers/api/users_controller.rb
784
- class Api::UsersController < ApplicationController
785
- def create
786
- schema = User.json_schema
787
-
788
- # Validate incoming request against the schema
789
- validation_result = JSONSchemer.schema(schema).valid?(params.to_json)
790
-
791
- if validation_result
792
- user = User.new(user_params)
793
- if user.save
794
- render json: user, status: :created
795
- else
796
- render json: { errors: user.errors }, status: :unprocessable_entity
797
- end
798
- else
799
- render json: { errors: "Invalid request" }, status: :bad_request
800
- end
801
- end
802
-
803
- private
804
-
805
- def user_params
806
- params.require(:user).permit(:name, :email, :password)
807
- end
808
- end
809
- ```
810
-
811
- ## Troubleshooting
812
-
813
- ### Common Errors
814
-
815
- #### "Invalid property name"
816
- Property names must start with a letter or underscore and can only contain letters, numbers, and underscores:
817
-
818
- ```ruby
819
- # Invalid
820
- property "1name", String # Starts with a number
821
- property "name!", String # Contains a special character
822
-
823
- # Valid
824
- property :name, String
825
- property :user_name, String
826
- ```
827
-
828
- #### "Property type is missing"
829
- You must specify a type for each property:
830
-
831
- ```ruby
832
- # Invalid
833
- property :name
834
-
835
- # Valid
836
- property :name, String
837
- ```
838
-
839
- #### "Unknown option"
840
- You specified an option that is not valid for the property type:
841
-
842
- ```ruby
843
- # Invalid (min_length is for strings, not integers)
844
- property :age, Integer, min_length: 2
845
-
846
- # Valid
847
- property :age, Integer, minimum: 18
848
- ```
849
-
850
- ### Schema Validation Issues
851
- If you're having issues with validation:
852
-
853
- 1. Make sure you've defined ActiveModel validations for your model
854
- 2. Check for mismatches between schema constraints and validations
855
- 3. Verify that required properties are present
856
-
857
- ### Type Errors
858
- Type errors usually occur when there's a mismatch between a property type and its constraints:
859
-
860
- ```ruby
861
- # Error: enum values must be strings for a string property
862
- property :status, String, enum: [1, 2, 3]
863
-
864
- # Correct
865
- property :status, String, enum: ["active", "inactive", "pending"]
866
- ```
867
-
868
- ### Best Practices
869
-
870
- 1. **Define clear property names and descriptions** for better documentation
871
- 2. **Use appropriate types** for each property with proper constraints
872
- 3. **Leverage automatic validations** by defining schema constraints instead of manual validations
873
- 4. **Keep schemas focused and modular** - extract nested objects to separate classes
874
- 5. **Reuse models when appropriate** instead of duplicating schema definitions
875
- 6. **Use explicit types** instead of relying on inference
876
- 7. **Test your schemas with sample data** to ensure validations work as expected
877
- 8. **Configure auto-validations globally** to maintain consistency across your application
878
- 9. **Use nullable_optional_property** for fields that can be omitted or null
879
- 10. **Document breaking changes** when updating schema definitions
880
-
881
- # Nullable vs Optional Properties in EasyTalk
882
-
883
- 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.
884
-
885
- ## Key Concepts
886
-
887
- | Concept | Description | JSON Schema Effect | EasyTalk Syntax |
888
- |---------|-------------|-------------------|-----------------|
889
- | **Nullable** | Property can have a `null` value | Adds `"null"` to the type array | `T.nilable(Type)` |
890
- | **Optional** | Property doesn't have to exist | Omits property from `"required"` array | `optional: true` constraint |
891
-
892
- ## Nullable Properties
893
-
894
- A **nullable** property can contain a `null` value, but the property itself must still be present in the object:
895
-
896
- ```ruby
897
- property :age, T.nilable(Integer)
898
- ```
899
-
900
- This produces the following JSON Schema:
901
-
902
- ```json
903
- {
904
- "properties": {
905
- "age": { "type": ["integer", "null"] }
906
- },
907
- "required": ["age"]
908
- }
909
- ```
910
-
911
- In this case, the following data would be valid:
912
- - `{ "age": 25 }`
913
- - `{ "age": null }`
914
-
915
- But this would be invalid:
916
- - `{ }` (missing the age property entirely)
917
-
918
- ## Optional Properties
919
-
920
- An **optional** property doesn't have to be present in the object at all:
921
-
922
- ```ruby
923
- property :nickname, String, optional: true
924
- ```
925
-
926
- This produces:
927
-
928
- ```json
929
- {
930
- "properties": {
931
- "nickname": { "type": "string" }
932
- }
933
- // Note: "nickname" is not in the "required" array
934
- }
935
- ```
936
-
937
- In this case, the following data would be valid:
938
- - `{ "nickname": "Joe" }`
939
- - `{ }` (omitting nickname entirely)
940
-
941
- But this would be invalid:
942
- - `{ "nickname": null }` (null is not allowed because the property isn't nullable)
943
-
944
- ## Nullable AND Optional Properties
945
-
946
- For properties that should be both nullable and optional (can be omitted or null), you need to combine both approaches:
947
-
948
- ```ruby
949
- property :bio, T.nilable(String), optional: true
950
- ```
951
-
952
- This produces:
953
-
954
- ```json
955
- {
956
- "properties": {
957
- "bio": { "type": ["string", "null"] }
958
- }
959
- // Note: "bio" is not in the "required" array
960
- }
961
- ```
962
-
963
- For convenience, EasyTalk also provides a helper method:
964
-
965
- ```ruby
966
- nullable_optional_property :bio, String
967
- ```
968
-
969
- Which is equivalent to the above.
970
-
971
- ## Configuration Options
972
-
973
- By default, nullable properties are still required. You can change this global behavior:
974
-
975
- ```ruby
976
- EasyTalk.configure do |config|
977
- config.nilable_is_optional = true # Makes all T.nilable properties also optional
978
- end
979
- ```
980
-
981
- With this configuration, any property defined with `T.nilable(Type)` will be treated as both nullable and optional.
982
-
983
- ## Practical Examples
984
-
985
- ### User Profile Schema
986
-
987
- ```ruby
988
- class UserProfile
989
- include EasyTalk::Model
990
-
991
- define_schema do
992
- # Required properties (must exist, cannot be null)
993
- property :id, String
994
- property :name, String
995
-
996
- # Required but nullable (must exist, can be null)
997
- property :age, T.nilable(Integer)
998
-
999
- # Optional but not nullable (can be omitted, cannot be null if present)
1000
- property :email, String, optional: true
1001
-
1002
- # Optional and nullable (can be omitted, can be null if present)
1003
- nullable_optional_property :bio, String
1004
- end
1005
- end
1006
- ```
1007
-
1008
- This creates clear expectations for data validation:
1009
- - `id` and `name` must be present and cannot be null
1010
- - `age` must be present but can be null
1011
- - `email` doesn't have to be present, but if it is, it cannot be null
1012
- - `bio` doesn't have to be present, and if it is, it can be null
1013
-
1014
- ## Common Gotchas
1015
-
1016
- ### Misconception: Nullable Implies Optional
1017
-
1018
- 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.
1019
-
1020
- ### Misconception: Optional Properties Accept Null
1021
-
1022
- 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)`.
1023
-
1024
- ## Migration from Earlier Versions
1025
-
1026
- 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.
1027
-
1028
- To maintain backward compatibility with your existing code, you can use:
1029
-
1030
- ```ruby
1031
- EasyTalk.configure do |config|
1032
- config.nilable_is_optional = true # Makes T.nilable properties behave as they did before
1033
- end
1034
- ```
1035
-
1036
- We recommend updating your schema definitions to explicitly declare which properties are optional using the `optional: true` constraint, as this makes your intent clearer.
1037
-
1038
- ## Best Practices
1039
-
1040
- 1. **Be explicit about intent**: Always clarify whether properties should be nullable, optional, or both
1041
- 2. **Use the helper method**: For properties that are both nullable and optional, use `nullable_optional_property`
1042
- 3. **Document expectations**: Use comments to clarify validation requirements for complex schemas
1043
- 4. **Consider validation implications**: Remember that ActiveModel validations operate independently of the schema definition
1044
-
1045
- ## JSON Schema Comparison
1046
-
1047
- | EasyTalk Definition | Required | Nullable | JSON Schema Equivalent |
1048
- |--------------------|----------|----------|------------------------|
1049
- | `property :p, String` | Yes | No | `{ "properties": { "p": { "type": "string" } }, "required": ["p"] }` |
1050
- | `property :p, T.nilable(String)` | Yes | Yes | `{ "properties": { "p": { "type": ["string", "null"] } }, "required": ["p"] }` |
1051
- | `property :p, String, optional: true` | No | No | `{ "properties": { "p": { "type": "string" } } }` |
1052
- | `nullable_optional_property :p, String` | No | Yes | `{ "properties": { "p": { "type": ["string", "null"] } } }` |
1053
-
1054
- ## Migration Guide from v1.x to v2.0
1055
-
1056
- ### Breaking Changes Summary
1057
-
1058
- 1. **Removed Block-Style Sub-Schemas**: Hash-based nested definitions are no longer supported
1059
- 2. **Enhanced Validation System**: Automatic validation generation is now enabled by default
1060
- 3. **Improved Model Initialization**: Better support for nested model instantiation
1061
-
1062
- ### Migration Steps
1063
-
1064
- #### 1. Replace Hash-based Nested Schemas
1065
-
1066
- ```ruby
1067
- # OLD (v1.x) - No longer works
1068
- class User
1069
- include EasyTalk::Model
1070
- define_schema do
1071
- property :address, Hash do
1072
- property :street, String
1073
- property :city, String
1074
- end
1075
- end
1076
- end
1077
-
1078
- # NEW (v2.x) - Extract to separate classes
1079
- class Address
1080
- include EasyTalk::Model
1081
- define_schema do
1082
- property :street, String
1083
- property :city, String
1084
- end
1085
- end
1086
-
1087
- class User
1088
- include EasyTalk::Model
1089
- define_schema do
1090
- property :address, Address
1091
- end
1092
- end
1093
- ```
1094
-
1095
- #### 2. Review Automatic Validations
1096
-
1097
- With `auto_validations: true` (default), you may need to remove redundant manual validations:
1098
-
1099
- ```ruby
1100
- # OLD (v1.x) - Manual validations required
1101
- class User
1102
- include EasyTalk::Model
1103
-
1104
- validates :name, presence: true, length: { minimum: 2 }
1105
- validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
1106
-
1107
- define_schema do
1108
- property :name, String
1109
- property :email, String
1110
- end
1111
- end
1112
-
1113
- # NEW (v2.x) - Automatic validations from constraints
1114
- class User
1115
- include EasyTalk::Model
1116
-
1117
- # Only add validations not covered by schema constraints
1118
- validates :email, uniqueness: true
1119
-
1120
- define_schema do
1121
- property :name, String, min_length: 2 # Auto-generates presence + length
1122
- property :email, String, format: "email" # Auto-generates presence + format
1123
- end
1124
- end
1125
- ```
1126
-
1127
- #### 3. Configuration Updates
1128
-
1129
- Review your configuration for new options:
1130
-
1131
- ```ruby
1132
- EasyTalk.configure do |config|
1133
- # New option in v2.0
1134
- config.auto_validations = true # Enable/disable automatic validation generation
1135
-
1136
- # Existing options (unchanged)
1137
- config.nilable_is_optional = false
1138
- config.default_additional_properties = false
1139
- # ... other existing config
1140
- end
1141
- ```
1142
-
1143
- ### Compatibility Notes
1144
-
1145
- - **Ruby Version**: Still requires Ruby 3.2+
1146
- - **Dependencies**: Core dependencies remain the same
1147
- - **JSON Schema Output**: No changes to generated schemas
1148
- - **ActiveModel Integration**: Fully backward compatible
1149
-
1150
- ## Development and Contributing
1151
-
1152
- ### Setting Up the Development Environment
1153
- 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.
1154
-
1155
- To install this gem onto your local machine, run:
1156
-
1157
- ```bash
1158
- bundle exec rake install
1159
- ```
1160
-
1161
- ### Running Tests
1162
- Run the test suite with:
1163
-
1164
- ```bash
1165
- bundle exec rake spec
1166
- ```
1167
-
1168
- ### Code Quality
1169
- Run the linter:
1170
-
1171
- ```bash
1172
- bundle exec rubocop
1173
- ```
1174
-
1175
- ### Contributing Guidelines
1176
- Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
1177
-
1178
- ## JSON Schema Version (`$schema` Keyword)
1179
-
1180
- 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.
1181
-
1182
- ### Why Use `$schema`?
1183
-
1184
- The `$schema` keyword:
1185
- - Declares the JSON Schema version your schema is written against
1186
- - Helps validators understand which specification to use
1187
- - Enables tooling to provide appropriate validation and autocomplete
1188
- - Documents the schema dialect for consumers of your API
1189
-
1190
- ### Supported Draft Versions
1191
-
1192
- EasyTalk supports the following JSON Schema draft versions:
1193
-
1194
- | Symbol | JSON Schema Version | URI |
1195
- |--------|---------------------|-----|
1196
- | `:draft202012` | Draft 2020-12 (latest) | `https://json-schema.org/draft/2020-12/schema` |
1197
- | `:draft201909` | Draft 2019-09 | `https://json-schema.org/draft/2019-09/schema` |
1198
- | `:draft7` | Draft-07 | `http://json-schema.org/draft-07/schema#` |
1199
- | `:draft6` | Draft-06 | `http://json-schema.org/draft-06/schema#` |
1200
- | `:draft4` | Draft-04 | `http://json-schema.org/draft-04/schema#` |
1201
- | `:none` | No `$schema` output (default) | N/A |
1202
-
1203
- ### Global Configuration
1204
-
1205
- Configure the schema version globally to apply to all models:
1206
-
1207
- ```ruby
1208
- EasyTalk.configure do |config|
1209
- config.schema_version = :draft202012 # Use JSON Schema Draft 2020-12
1210
- end
1211
- ```
1212
-
1213
- With this configuration, all models will include `$schema` in their output:
1214
-
1215
- ```ruby
1216
- class User
692
+ class ShippingInfo
1217
693
  include EasyTalk::Model
1218
694
 
1219
695
  define_schema do
1220
696
  property :name, String
1221
- end
1222
- end
1223
-
1224
- User.json_schema
1225
- # => {
1226
- # "$schema" => "https://json-schema.org/draft/2020-12/schema",
1227
- # "type" => "object",
1228
- # "properties" => { "name" => { "type" => "string" } },
1229
- # "required" => ["name"],
1230
- # "additionalProperties" => false
1231
- # }
1232
- ```
1233
-
1234
- ### Per-Model Configuration
1235
-
1236
- Override the global setting for individual models using the `schema_version` keyword in the schema definition:
1237
-
1238
- ```ruby
1239
- class LegacyModel
1240
- include EasyTalk::Model
697
+ property :credit_card, String, optional: true
698
+ property :billing_address, String, optional: true
1241
699
 
1242
- define_schema do
1243
- schema_version :draft7 # Use Draft-07 for this specific model
1244
- property :name, String
700
+ # If credit_card is present, billing_address is required
701
+ dependent_required(
702
+ 'credit_card' => ['billing_address']
703
+ )
1245
704
  end
1246
705
  end
1247
-
1248
- LegacyModel.json_schema
1249
- # => {
1250
- # "$schema" => "http://json-schema.org/draft-07/schema#",
1251
- # "type" => "object",
1252
- # ...
1253
- # }
1254
706
  ```
1255
707
 
1256
- ### Disabling `$schema` for Specific Models
708
+ ### Custom type builders
1257
709
 
1258
- If you have a global schema version configured but want to exclude `$schema` from a specific model, use `:none`:
710
+ Register custom types with their own schema builders:
1259
711
 
1260
712
  ```ruby
1261
713
  EasyTalk.configure do |config|
1262
- config.schema_version = :draft202012 # Global default
1263
- end
1264
-
1265
- class InternalModel
1266
- include EasyTalk::Model
1267
-
1268
- define_schema do
1269
- schema_version :none # No $schema for this model
1270
- property :data, String
1271
- end
714
+ config.register_type(Money, MoneySchemaBuilder)
1272
715
  end
1273
716
 
1274
- InternalModel.json_schema
1275
- # => {
1276
- # "type" => "object",
1277
- # "properties" => { "data" => { "type" => "string" } },
1278
- # ...
1279
- # }
1280
- # Note: No "$schema" key present
717
+ # Or directly:
718
+ EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
1281
719
  ```
1282
720
 
1283
- ### Custom Schema URIs
1284
-
1285
- You can also specify a custom URI if you're using a custom meta-schema or a different schema registry:
1286
-
1287
- ```ruby
1288
- class CustomModel
1289
- include EasyTalk::Model
1290
-
1291
- define_schema do
1292
- schema_version 'https://my-company.com/schemas/v1/meta-schema.json'
1293
- property :id, String
1294
- end
1295
- end
1296
- ```
1297
-
1298
- ### Nested Models
1299
-
1300
- 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`:
1301
-
1302
- ```ruby
1303
- EasyTalk.configure do |config|
1304
- config.schema_version = :draft202012
1305
- end
1306
-
1307
- class Address
1308
- include EasyTalk::Model
1309
- define_schema do
1310
- property :city, String
1311
- end
1312
- end
1313
-
1314
- class User
1315
- include EasyTalk::Model
1316
- define_schema do
1317
- property :name, String
1318
- property :address, Address
1319
- end
1320
- end
1321
-
1322
- User.json_schema
1323
- # => {
1324
- # "$schema" => "https://json-schema.org/draft/2020-12/schema", # Only at root
1325
- # "type" => "object",
1326
- # "properties" => {
1327
- # "name" => { "type" => "string" },
1328
- # "address" => {
1329
- # "type" => "object", # No $schema here
1330
- # "properties" => { "city" => { "type" => "string" } },
1331
- # ...
1332
- # }
1333
- # },
1334
- # ...
1335
- # }
1336
- ```
1337
-
1338
- ### Default Behavior
1339
-
1340
- 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.
1341
-
1342
- ### Best Practices
1343
-
1344
- 1. **Choose a version appropriate for your validators**: If you're using a specific JSON Schema validator, check which drafts it supports.
1345
-
1346
- 2. **Use Draft 2020-12 for new projects**: It's the latest stable version with the most features.
1347
-
1348
- 3. **Be consistent**: Use global configuration for consistency across your application, and only override per-model when necessary.
1349
-
1350
- 4. **Consider your consumers**: If your schemas are consumed by external systems, ensure they support the draft version you're using.
1351
-
1352
- ## Schema Identifier (`$id` Keyword)
1353
-
1354
- The `$id` keyword provides a unique identifier for your JSON Schema document. EasyTalk supports configuring this at both the global and per-model level.
1355
-
1356
- ### Why Use `$id`?
1357
-
1358
- The `$id` keyword:
1359
- - Establishes a unique URI identifier for the schema
1360
- - Enables referencing schemas from other documents via `$ref`
1361
- - Provides a base URI for resolving relative references within the schema
1362
- - Documents the canonical location of the schema
1363
-
1364
- ### Global Configuration
1365
-
1366
- Configure the schema ID globally to apply to all models:
1367
-
1368
- ```ruby
1369
- EasyTalk.configure do |config|
1370
- config.schema_id = 'https://example.com/schemas/base.json'
1371
- end
1372
- ```
1373
-
1374
- With this configuration, all models will include `$id` in their output:
1375
-
1376
- ```ruby
1377
- class User
1378
- include EasyTalk::Model
1379
-
1380
- define_schema do
1381
- property :name, String
1382
- end
1383
- end
1384
-
1385
- User.json_schema
1386
- # => {
1387
- # "$id" => "https://example.com/schemas/base.json",
1388
- # "type" => "object",
1389
- # "properties" => { "name" => { "type" => "string" } },
1390
- # "required" => ["name"],
1391
- # "additionalProperties" => false
1392
- # }
1393
- ```
1394
-
1395
- ### Per-Model Configuration
1396
-
1397
- Override the global setting for individual models using the `schema_id` keyword in the schema definition:
1398
-
1399
- ```ruby
1400
- class User
1401
- include EasyTalk::Model
1402
-
1403
- define_schema do
1404
- schema_id 'https://example.com/schemas/user.schema.json'
1405
- property :name, String
1406
- property :email, String
1407
- end
1408
- end
1409
-
1410
- User.json_schema
1411
- # => {
1412
- # "$id" => "https://example.com/schemas/user.schema.json",
1413
- # "type" => "object",
1414
- # ...
1415
- # }
1416
- ```
1417
-
1418
- ### Disabling `$id` for Specific Models
1419
-
1420
- If you have a global schema ID configured but want to exclude `$id` from a specific model, use `:none`:
1421
-
1422
- ```ruby
1423
- EasyTalk.configure do |config|
1424
- config.schema_id = 'https://example.com/schemas/default.json'
1425
- end
1426
-
1427
- class InternalModel
1428
- include EasyTalk::Model
1429
-
1430
- define_schema do
1431
- schema_id :none # No $id for this model
1432
- property :data, String
1433
- end
1434
- end
1435
-
1436
- InternalModel.json_schema
1437
- # => {
1438
- # "type" => "object",
1439
- # "properties" => { "data" => { "type" => "string" } },
1440
- # ...
1441
- # }
1442
- # Note: No "$id" key present
1443
- ```
1444
-
1445
- ### Combining `$schema` and `$id`
1446
-
1447
- When both `$schema` and `$id` are configured, they appear in the standard order (`$schema` first, then `$id`):
1448
-
1449
- ```ruby
1450
- class Product
1451
- include EasyTalk::Model
1452
-
1453
- define_schema do
1454
- schema_version :draft202012
1455
- schema_id 'https://example.com/schemas/product.schema.json'
1456
- property :name, String
1457
- property :price, Float
1458
- end
1459
- end
1460
-
1461
- Product.json_schema
1462
- # => {
1463
- # "$schema" => "https://json-schema.org/draft/2020-12/schema",
1464
- # "$id" => "https://example.com/schemas/product.schema.json",
1465
- # "type" => "object",
1466
- # ...
1467
- # }
1468
- ```
1469
-
1470
- ### Nested Models
1471
-
1472
- 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`:
1473
-
1474
- ```ruby
1475
- EasyTalk.configure do |config|
1476
- config.schema_id = 'https://example.com/schemas/user.json'
1477
- end
1478
-
1479
- class Address
1480
- include EasyTalk::Model
1481
- define_schema do
1482
- property :city, String
1483
- end
1484
- end
1485
-
1486
- class User
1487
- include EasyTalk::Model
1488
- define_schema do
1489
- property :name, String
1490
- property :address, Address
1491
- end
1492
- end
1493
-
1494
- User.json_schema
1495
- # => {
1496
- # "$id" => "https://example.com/schemas/user.json", # Only at root
1497
- # "type" => "object",
1498
- # "properties" => {
1499
- # "name" => { "type" => "string" },
1500
- # "address" => {
1501
- # "type" => "object", # No $id here
1502
- # "properties" => { "city" => { "type" => "string" } },
1503
- # ...
1504
- # }
1505
- # },
1506
- # ...
1507
- # }
1508
- ```
1509
-
1510
- ### URI Formats
1511
-
1512
- The `$id` accepts various URI formats:
1513
-
1514
- ```ruby
1515
- # Absolute URI (recommended for published schemas)
1516
- schema_id 'https://example.com/schemas/user.schema.json'
1517
-
1518
- # Relative URI
1519
- schema_id 'user.schema.json'
1520
-
1521
- # URN format
1522
- schema_id 'urn:example:user-schema'
1523
- ```
1524
-
1525
- ### Default Behavior
1526
-
1527
- 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.
1528
-
1529
- ### Best Practices
1530
-
1531
- 1. **Use absolute URIs for published schemas**: This ensures global uniqueness and enables external references.
1532
-
1533
- 2. **Follow a consistent naming convention**: For example, `https://yourdomain.com/schemas/{model-name}.schema.json`.
1534
-
1535
- 3. **Keep IDs stable**: Once published, avoid changing schema IDs as external systems may reference them.
1536
-
1537
- 4. **Combine with `$schema`**: When publishing schemas, include both `$schema` (for validation) and `$id` (for identification).
1538
-
1539
- ## Schema References (`$ref` and `$defs`)
1540
-
1541
- 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.
1542
-
1543
- ### Why Use `$ref`?
1544
-
1545
- The `$ref` keyword:
1546
- - Reduces schema duplication when the same model appears multiple times
1547
- - Produces cleaner, more organized schemas
1548
- - Improves schema readability and maintainability
1549
- - Aligns with JSON Schema best practices for reusable definitions
1550
-
1551
- ### Default Behavior (Inline Schemas)
1552
-
1553
- By default, EasyTalk inlines nested model schemas directly:
1554
-
1555
- ```ruby
1556
- class Address
1557
- include EasyTalk::Model
1558
- define_schema do
1559
- property :street, String
1560
- property :city, String
1561
- end
1562
- end
1563
-
1564
- class Person
1565
- include EasyTalk::Model
1566
- define_schema do
1567
- property :name, String
1568
- property :address, Address
1569
- end
1570
- end
1571
-
1572
- Person.json_schema
1573
- # => {
1574
- # "type" => "object",
1575
- # "properties" => {
1576
- # "name" => { "type" => "string" },
1577
- # "address" => {
1578
- # "type" => "object",
1579
- # "properties" => {
1580
- # "street" => { "type" => "string" },
1581
- # "city" => { "type" => "string" }
1582
- # },
1583
- # ...
1584
- # }
1585
- # },
1586
- # ...
1587
- # }
1588
- ```
1589
-
1590
- ### Enabling `$ref` References
1591
-
1592
- #### Global Configuration
1593
-
1594
- Enable `$ref` generation for all nested models:
1595
-
1596
- ```ruby
1597
- EasyTalk.configure do |config|
1598
- config.use_refs = true
1599
- end
1600
- ```
1601
-
1602
- With this configuration, nested models are referenced via `$ref` and their definitions are placed in `$defs`:
1603
-
1604
- ```ruby
1605
- Person.json_schema
1606
- # => {
1607
- # "type" => "object",
1608
- # "properties" => {
1609
- # "name" => { "type" => "string" },
1610
- # "address" => { "$ref" => "#/$defs/Address" }
1611
- # },
1612
- # "$defs" => {
1613
- # "Address" => {
1614
- # "type" => "object",
1615
- # "properties" => {
1616
- # "street" => { "type" => "string" },
1617
- # "city" => { "type" => "string" }
1618
- # },
1619
- # ...
1620
- # }
1621
- # },
1622
- # ...
1623
- # }
1624
- ```
1625
-
1626
- #### Per-Property Configuration
1627
-
1628
- You can also enable `$ref` for specific properties using the `ref: true` constraint:
1629
-
1630
- ```ruby
1631
- class Person
1632
- include EasyTalk::Model
1633
- define_schema do
1634
- property :name, String
1635
- property :address, Address, ref: true # Use $ref for this property
1636
- end
1637
- end
1638
- ```
1639
-
1640
- Or disable `$ref` for specific properties when it's enabled globally:
1641
-
1642
- ```ruby
1643
- EasyTalk.configure do |config|
1644
- config.use_refs = true
1645
- end
1646
-
1647
- class Person
1648
- include EasyTalk::Model
1649
- define_schema do
1650
- property :name, String
1651
- property :address, Address, ref: false # Inline this property despite global setting
1652
- end
1653
- end
1654
- ```
1655
-
1656
- ### Arrays of Models
1657
-
1658
- When using `$ref` with arrays of models, the `$ref` applies to the array items:
1659
-
1660
- ```ruby
1661
- EasyTalk.configure do |config|
1662
- config.use_refs = true
1663
- end
1664
-
1665
- class Company
1666
- include EasyTalk::Model
1667
- define_schema do
1668
- property :name, String
1669
- property :addresses, T::Array[Address]
1670
- end
1671
- end
1672
-
1673
- Company.json_schema
1674
- # => {
1675
- # "type" => "object",
1676
- # "properties" => {
1677
- # "name" => { "type" => "string" },
1678
- # "addresses" => {
1679
- # "type" => "array",
1680
- # "items" => { "$ref" => "#/$defs/Address" }
1681
- # }
1682
- # },
1683
- # "$defs" => {
1684
- # "Address" => { ... }
1685
- # },
1686
- # ...
1687
- # }
1688
- ```
1689
-
1690
- You can also use the per-property `ref` constraint with arrays:
1691
-
1692
- ```ruby
1693
- property :addresses, T::Array[Address], ref: true
1694
- ```
1695
-
1696
- ### Nilable Models with `$ref`
1697
-
1698
- When using `$ref` with nilable model types, EasyTalk uses `anyOf` to combine the reference with the null type:
1699
-
1700
- ```ruby
1701
- EasyTalk.configure do |config|
1702
- config.use_refs = true
1703
- end
1704
-
1705
- class Person
1706
- include EasyTalk::Model
1707
- define_schema do
1708
- property :name, String
1709
- property :address, T.nilable(Address)
1710
- end
1711
- end
1712
-
1713
- Person.json_schema
1714
- # => {
1715
- # "type" => "object",
1716
- # "properties" => {
1717
- # "name" => { "type" => "string" },
1718
- # "address" => {
1719
- # "anyOf" => [
1720
- # { "$ref" => "#/$defs/Address" },
1721
- # { "type" => "null" }
1722
- # ]
1723
- # }
1724
- # },
1725
- # "$defs" => {
1726
- # "Address" => { ... }
1727
- # },
1728
- # ...
1729
- # }
1730
- ```
1731
-
1732
- ### Multiple References to the Same Model
1733
-
1734
- When the same model is used multiple times, it only appears once in `$defs`:
1735
-
1736
- ```ruby
1737
- class Person
1738
- include EasyTalk::Model
1739
- define_schema do
1740
- property :name, String
1741
- property :home_address, Address, ref: true
1742
- property :work_address, Address, ref: true
1743
- property :shipping_addresses, T::Array[Address], ref: true
1744
- end
1745
- end
1746
-
1747
- Person.json_schema
1748
- # => {
1749
- # "type" => "object",
1750
- # "properties" => {
1751
- # "name" => { "type" => "string" },
1752
- # "home_address" => { "$ref" => "#/$defs/Address" },
1753
- # "work_address" => { "$ref" => "#/$defs/Address" },
1754
- # "shipping_addresses" => {
1755
- # "type" => "array",
1756
- # "items" => { "$ref" => "#/$defs/Address" }
1757
- # }
1758
- # },
1759
- # "$defs" => {
1760
- # "Address" => { ... } # Only defined once
1761
- # },
1762
- # ...
1763
- # }
1764
- ```
1765
-
1766
- ### Combining `$ref` with Other Constraints
1767
-
1768
- You can add additional constraints alongside `$ref`:
1769
-
1770
- ```ruby
1771
- class Person
1772
- include EasyTalk::Model
1773
- define_schema do
1774
- property :address, Address, ref: true, description: "Primary address", title: "Main Address"
1775
- end
1776
- end
1777
-
1778
- Person.json_schema["properties"]["address"]
1779
- # => {
1780
- # "$ref" => "#/$defs/Address",
1781
- # "description" => "Primary address",
1782
- # "title" => "Main Address"
1783
- # }
1784
- ```
1785
-
1786
- ### Interaction with `compose`
1787
-
1788
- When using `compose` with `T::AllOf`, `T::AnyOf`, or `T::OneOf`, the composed models are also placed in `$defs`:
1789
-
1790
- ```ruby
1791
- class Employee
1792
- include EasyTalk::Model
1793
- define_schema do
1794
- compose T::AllOf[Person, EmployeeDetails]
1795
- property :badge_number, String
1796
- end
1797
- end
1798
- ```
1799
-
1800
- If you also have properties using `$ref`, both the composed models and property models will appear in `$defs`.
1801
-
1802
- ### Best Practices
1803
-
1804
- 1. **Use global configuration for consistency**: If you prefer `$ref` style, enable it globally rather than per-property.
1805
-
1806
- 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.
1807
-
1808
- 3. **Use `$ref` for frequently reused models**: If a model appears in many places, `$ref` reduces schema size and improves maintainability.
721
+ See the [Custom Type Builders documentation](https://rubydoc.info/gems/easy_talk/EasyTalk/Builders/Registry) for details on creating builders.
1809
722
 
1810
- 4. **Keep inline for simple, single-use models**: For models used only once, inlining may be more readable.
723
+ ---
1811
724
 
1812
- ### Default Behavior
725
+ ## Known limitations
1813
726
 
1814
- By default, `use_refs` is set to `false`, meaning nested models are inlined. This maintains backward compatibility with previous versions of EasyTalk.
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
1815
731
 
1816
- ## JSON Schema Compatibility
732
+ ---
1817
733
 
1818
- ### Supported Versions
1819
- 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).
734
+ ## Contributing
1820
735
 
1821
- 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.
736
+ - Run `bin/setup`
737
+ - Run specs: `bundle exec rake spec`
738
+ - Run lint: `bundle exec rubocop`
1822
739
 
1823
- ### Specification Compliance
1824
- 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.
740
+ Bug reports and PRs welcome.
1825
741
 
1826
- ### Known Limitations
1827
- - Limited support for custom formats
1828
- - Some draft-specific keywords may not be supported
1829
- - Complex composition scenarios may require manual adjustment
742
+ ---
1830
743
 
1831
744
  ## License
1832
745
 
1833
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
746
+ MIT