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