easy_talk 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -39
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +164 -0
  5. data/README.md +442 -1529
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +169 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/primitive-schema-rfc.md +894 -0
  14. data/docs/property-types.markdown +212 -0
  15. data/docs/schema-definition.markdown +180 -0
  16. data/lib/easy_talk/builders/base_builder.rb +6 -3
  17. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  18. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  19. data/lib/easy_talk/builders/composition_builder.rb +16 -13
  20. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  21. data/lib/easy_talk/builders/null_builder.rb +4 -1
  22. data/lib/easy_talk/builders/number_builder.rb +4 -1
  23. data/lib/easy_talk/builders/object_builder.rb +109 -33
  24. data/lib/easy_talk/builders/registry.rb +182 -0
  25. data/lib/easy_talk/builders/string_builder.rb +3 -1
  26. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  27. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  28. data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
  29. data/lib/easy_talk/builders/union_builder.rb +5 -1
  30. data/lib/easy_talk/configuration.rb +47 -2
  31. data/lib/easy_talk/error_formatter/base.rb +100 -0
  32. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  33. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  34. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  35. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  36. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  37. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  38. data/lib/easy_talk/error_formatter.rb +143 -0
  39. data/lib/easy_talk/errors.rb +3 -0
  40. data/lib/easy_talk/errors_helper.rb +66 -34
  41. data/lib/easy_talk/json_schema_equality.rb +46 -0
  42. data/lib/easy_talk/keywords.rb +0 -1
  43. data/lib/easy_talk/model.rb +148 -89
  44. data/lib/easy_talk/model_helper.rb +17 -0
  45. data/lib/easy_talk/naming_strategies.rb +24 -0
  46. data/lib/easy_talk/property.rb +23 -94
  47. data/lib/easy_talk/ref_helper.rb +33 -0
  48. data/lib/easy_talk/schema.rb +199 -0
  49. data/lib/easy_talk/schema_definition.rb +57 -5
  50. data/lib/easy_talk/schema_methods.rb +111 -0
  51. data/lib/easy_talk/sorbet_extension.rb +1 -0
  52. data/lib/easy_talk/tools/function_builder.rb +1 -1
  53. data/lib/easy_talk/type_introspection.rb +222 -0
  54. data/lib/easy_talk/types/base_composer.rb +2 -1
  55. data/lib/easy_talk/types/composer.rb +4 -0
  56. data/lib/easy_talk/types/tuple.rb +77 -0
  57. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
  58. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  59. data/lib/easy_talk/validation_adapters/base.rb +156 -0
  60. data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
  61. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  62. data/lib/easy_talk/validation_builder.rb +29 -309
  63. data/lib/easy_talk/version.rb +1 -1
  64. data/lib/easy_talk.rb +42 -0
  65. metadata +38 -7
  66. data/docs/404.html +0 -25
  67. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  68. data/easy_talk.gemspec +0 -39
data/Rakefile CHANGED
@@ -3,9 +3,36 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
  require 'rubocop/rake_task'
6
+ require 'yard'
6
7
 
7
8
  RSpec::Core::RakeTask.new(:spec)
8
9
 
9
10
  RuboCop::RakeTask.new
10
11
 
12
+ YARD::Rake::YardocTask.new(:yard) do |t|
13
+ t.files = ['lib/**/*.rb']
14
+ t.options = ['--readme', 'README.md', '--output-dir', 'docs/api']
15
+ end
16
+
17
+ namespace :docs do
18
+ desc 'Generate YARD API documentation'
19
+ task api: :yard
20
+
21
+ desc 'Serve Jekyll documentation locally'
22
+ task :serve do
23
+ Dir.chdir('docs') do
24
+ sh 'bundle install --quiet'
25
+ sh 'bundle exec jekyll serve'
26
+ end
27
+ end
28
+
29
+ desc 'Build all documentation (Jekyll + YARD)'
30
+ task build: :yard do
31
+ Dir.chdir('docs') do
32
+ sh 'bundle install --quiet'
33
+ sh 'bundle exec jekyll build'
34
+ end
35
+ end
36
+ end
37
+
11
38
  task default: %i[spec rubocop]
data/docs/.gitignore CHANGED
@@ -3,3 +3,4 @@ _site
3
3
  .jekyll-cache
4
4
  .jekyll-metadata
5
5
  vendor
6
+ api/
data/docs/about.markdown CHANGED
@@ -4,15 +4,35 @@ title: About
4
4
  permalink: /about/
5
5
  ---
6
6
 
7
- This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/)
7
+ # About EasyTalk
8
8
 
9
- You can find the source code for Minima at GitHub:
10
- [jekyll][jekyll-organization] /
11
- [minima](https://github.com/jekyll/minima)
9
+ EasyTalk is a Ruby library for defining and generating JSON Schema from Ruby classes. Inspired by Python's Pydantic library, it provides an ActiveModel-like interface for defining structured data models with automatic validation and JSON Schema generation.
12
10
 
13
- You can find the source code for Jekyll at GitHub:
14
- [jekyll][jekyll-organization] /
15
- [jekyll](https://github.com/jekyll/jekyll)
11
+ ## Why EasyTalk?
16
12
 
13
+ - **Type Safety** - Define your data structures once and get validation automatically
14
+ - **JSON Schema Generation** - Produce standards-compliant JSON Schema from Ruby code
15
+ - **LLM Integration** - Generate function schemas for OpenAI and other LLM providers
16
+ - **ActiveModel Compatible** - Works with Rails forms, validations, and serialization
17
17
 
18
- [jekyll-organization]: https://github.com/jekyll
18
+ ## Use Cases
19
+
20
+ - **API Request/Response Validation** - Define expected shapes for API payloads
21
+ - **LLM Function Calling** - Generate structured output schemas for AI applications
22
+ - **Configuration Files** - Validate configuration against a schema
23
+ - **Data Pipelines** - Ensure data conforms to expected structures
24
+
25
+ ## Links
26
+
27
+ - [GitHub Repository](https://github.com/sergiobayona/easy_talk)
28
+ - [RubyGems](https://rubygems.org/gems/easy_talk)
29
+ - [API Documentation](api/)
30
+ - [Changelog](https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md)
31
+
32
+ ## Author
33
+
34
+ Created by [Sergio Bayona](https://github.com/sergiobayona).
35
+
36
+ ## License
37
+
38
+ EasyTalk is released under the [MIT License](https://github.com/sergiobayona/easy_talk/blob/main/LICENSE.txt).
@@ -0,0 +1,102 @@
1
+ ---
2
+ layout: page
3
+ title: Getting Started
4
+ permalink: /getting-started/
5
+ ---
6
+
7
+ # Getting Started with EasyTalk
8
+
9
+ ## Requirements
10
+
11
+ - Ruby 3.2 or later
12
+ - ActiveModel/ActiveSupport 7.0-8.x
13
+
14
+ ## Installation
15
+
16
+ Add EasyTalk to your Gemfile:
17
+
18
+ ```ruby
19
+ gem 'easy_talk'
20
+ ```
21
+
22
+ Then run:
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ Or install directly:
29
+
30
+ ```bash
31
+ gem install easy_talk
32
+ ```
33
+
34
+ ## Basic Usage
35
+
36
+ ### 1. Define a Model
37
+
38
+ Include `EasyTalk::Model` in your class and use `define_schema` to declare properties:
39
+
40
+ ```ruby
41
+ require 'easy_talk'
42
+
43
+ class Person
44
+ include EasyTalk::Model
45
+
46
+ define_schema do
47
+ title "Person"
48
+ description "A person record"
49
+
50
+ property :name, String
51
+ property :age, Integer
52
+ property :email, String, format: "email"
53
+ end
54
+ end
55
+ ```
56
+
57
+ ### 2. Generate JSON Schema
58
+
59
+ Call `.json_schema` on your class to get the JSON Schema:
60
+
61
+ ```ruby
62
+ Person.json_schema
63
+ ```
64
+
65
+ This produces:
66
+
67
+ ```json
68
+ {
69
+ "type": "object",
70
+ "title": "Person",
71
+ "description": "A person record",
72
+ "properties": {
73
+ "name": { "type": "string" },
74
+ "age": { "type": "integer" },
75
+ "email": { "type": "string", "format": "email" }
76
+ },
77
+ "required": ["name", "age", "email"],
78
+ "additionalProperties": false
79
+ }
80
+ ```
81
+
82
+ ### 3. Create and Validate Instances
83
+
84
+ EasyTalk models work like ActiveModel objects:
85
+
86
+ ```ruby
87
+ person = Person.new(name: "Alice", age: 30, email: "alice@example.com")
88
+ person.valid? # => true
89
+ person.name # => "Alice"
90
+
91
+ # Invalid data triggers validation errors
92
+ invalid = Person.new(name: "", age: -5, email: "not-an-email")
93
+ invalid.valid? # => false
94
+ invalid.errors.full_messages
95
+ # => ["Name is too short", "Age must be greater than or equal to 0", ...]
96
+ ```
97
+
98
+ ## Next Steps
99
+
100
+ - [Schema Definition](schema-definition) - Learn the full DSL
101
+ - [Property Types](property-types) - Explore available types and constraints
102
+ - [Nested Models](nested-models) - Build complex, composable schemas
data/docs/index.markdown CHANGED
@@ -1,7 +1,54 @@
1
1
  ---
2
- # Feel free to add content and custom Front Matter to this file.
3
- # To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
4
-
5
2
  layout: home
3
+ title: Home
6
4
  ---
7
- EasyTalk is a Ruby library that simplifies defining and generating JSON Schema documents, and validates that JSON data conforms to these schemas.
5
+
6
+ # EasyTalk
7
+
8
+ EasyTalk is a Ruby library for defining and generating JSON Schema from Ruby classes. It provides an ActiveModel-like interface for defining structured data models with validation and JSON Schema generation capabilities.
9
+
10
+ ## Key Features
11
+
12
+ - **Schema Definition DSL** - Define JSON Schema using a clean Ruby DSL
13
+ - **Type System** - Support for Ruby types and Sorbet-style generics
14
+ - **ActiveModel Integration** - Built-in validations from schema constraints
15
+ - **Nested Models** - Compose complex schemas from simple building blocks
16
+ - **LLM Function Calling** - Generate OpenAI-compatible function schemas
17
+
18
+ ## Quick Start
19
+
20
+ ```ruby
21
+ class User
22
+ include EasyTalk::Model
23
+
24
+ define_schema do
25
+ title "User"
26
+ description "A user in the system"
27
+ property :name, String, min_length: 2
28
+ property :email, String, format: "email"
29
+ property :age, Integer, minimum: 0, optional: true
30
+ end
31
+ end
32
+
33
+ # Generate JSON Schema
34
+ User.json_schema
35
+ # => {"type"=>"object", "title"=>"User", ...}
36
+
37
+ # Create and validate instances
38
+ user = User.new(name: "Alice", email: "alice@example.com")
39
+ user.valid? # => true
40
+ ```
41
+
42
+ ## Documentation
43
+
44
+ - [Getting Started](getting-started) - Installation and basic usage
45
+ - [Schema Definition](schema-definition) - How to define schemas
46
+ - [Property Types](property-types) - Available types and constraints
47
+ - [Nested Models](nested-models) - Composing complex schemas
48
+ - [API Reference](api/) - Generated API documentation
49
+
50
+ ## Links
51
+
52
+ - [GitHub Repository](https://github.com/sergiobayona/easy_talk)
53
+ - [RubyGems](https://rubygems.org/gems/easy_talk)
54
+ - [Changelog](https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md)
@@ -0,0 +1,169 @@
1
+ # JSON Schema Compliance Guide
2
+
3
+ This document defines the strategy for testing and improving `EasyTalk`'s compliance with the official [JSON Schema Specification](https://json-schema.org/specification).
4
+
5
+ ## Test Suite Setup
6
+
7
+ `EasyTalk` integrates the standard [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) as a git submodule in `spec/fixtures/json_schema_test_suite`. This provides a language-agnostic set of test cases covering all aspects of the specification.
8
+
9
+ ### Infrastructure
10
+
11
+ - **Submodule**: Located at `spec/fixtures/json_schema_test_suite`.
12
+ - **Converter**: `spec/support/json_schema_converter.rb` dynamically converts raw JSON Schema definitions into `EasyTalk::Model` classes at runtime.
13
+ - **Runner**: `spec/integration/json_schema_compliance_spec.rb` iterates over the test suite files, generates models, and asserts valid/invalid behavior.
14
+
15
+ ## Running the Tests
16
+
17
+ The compliance tests are **optional** and excluded from the default `rspec` run to prevent noise from known incompatibilities.
18
+
19
+ To run the compliance suite:
20
+
21
+ ```bash
22
+ bundle exec rspec --tag json_schema_compliance spec/integration/json_schema_compliance_spec.rb
23
+ ```
24
+
25
+ ## Schema Wrapping Strategy
26
+
27
+ Since `EasyTalk` models are always objects, the JSON Schema test suite's root-level primitive tests (e.g., `{"type": "integer"}` with data `5`) require adaptation.
28
+
29
+ ### How It Works
30
+
31
+ The `JsonSchemaConverter` uses a **wrapper property strategy**:
32
+
33
+ 1. **Detection**: `needs_wrapping?` checks if the schema is non-object (no `type: object` or `properties` key)
34
+ 2. **Wrapping**: Non-object schemas become a `value` property on a wrapper object
35
+ 3. **Data transformation**: Primitive test data is wrapped as `{"value": data}`
36
+
37
+ **Example transformation:**
38
+
39
+ ```
40
+ Original JSON Schema test:
41
+ Schema: {"type": "integer", "minimum": 1}
42
+ Data: 5
43
+ Valid: true
44
+
45
+ Transformed for EasyTalk:
46
+ Schema: {
47
+ "type": "object",
48
+ "properties": { "value": {"type": "integer", "minimum": 1} },
49
+ "required": ["value"]
50
+ }
51
+ Data: {"value": 5}
52
+ Valid: true
53
+ ```
54
+
55
+ This preserves validation semantics while fitting EasyTalk's object-based model.
56
+
57
+ ## Current Test Results
58
+
59
+ As of the latest run:
60
+
61
+ | Metric | Count |
62
+ |--------|-------|
63
+ | Total examples | 916 |
64
+ | Passing | ~193 |
65
+ | Failing | 165 |
66
+ | Pending (known unsupported) | 558 |
67
+
68
+ ### Known Unsupported Features
69
+
70
+ The following test files are skipped entirely via `KNOWN_FAILURES`:
71
+
72
+ | File | Reason |
73
+ |------|--------|
74
+ | `not.json` | `not` keyword not supported |
75
+ | `anyOf.json` | `anyOf` validation not supported |
76
+ | `allOf.json` | `allOf` validation not supported |
77
+ | `oneOf.json` | `oneOf` validation not supported |
78
+ | `refRemote.json` | Remote `$ref` not supported |
79
+ | `dependencies.json` | Dependencies not supported |
80
+ | `definitions.json` | `$defs`/definitions not supported |
81
+ | `if-then-else.json` | Conditional logic not supported |
82
+ | `patternProperties.json` | Pattern properties not supported |
83
+ | `properties.json` | Complex property interactions not supported |
84
+ | `propertyNames.json` | Property names validation not supported |
85
+ | `ref.json` | Complex `$ref` not supported |
86
+ | `required.json` | Complex required checks not supported |
87
+ | `additionalItems.json` | Additional items not supported |
88
+ | `additionalProperties.json` | Additional properties validation not supported |
89
+ | `boolean_schema.json` | Boolean schemas (`true`/`false` as schema) not supported |
90
+ | `const.json` | `const` keyword not supported |
91
+ | `default.json` | Default keyword behavior not supported |
92
+ | `enum.json` | Enum validation not fully supported |
93
+ | `infinite-loop-detection.json` | Infinite loop detection not supported |
94
+ | `maxProperties.json` | Max properties not supported |
95
+ | `minProperties.json` | Min properties not supported |
96
+
97
+ ## Compliance Gaps
98
+
99
+ The 165 failing tests reveal real validation gaps in EasyTalk:
100
+
101
+ ### 1. Type Coercion (Intentional Behavior)
102
+
103
+ EasyTalk uses ActiveModel's numericality validation which coerces strings to numbers:
104
+
105
+ ```ruby
106
+ user = User.new(age: "30") # String
107
+ user.valid? # => true (coerced to integer 30)
108
+ ```
109
+
110
+ Per JSON Schema, `"30"` should be invalid for `type: integer`. This is documented as **intentional behavior** for Rails compatibility. A `strict_types` configuration option is planned (see [#137](https://github.com/sergiobayona/easy_talk/issues/137)).
111
+
112
+ ### 2. Format Validation Scope
113
+
114
+ JSON Schema specifies that format validations should only apply to strings and ignore other types. EasyTalk currently validates format on the assigned value regardless of type.
115
+
116
+ ### 3. Empty String Presence
117
+
118
+ EasyTalk uses ActiveModel's presence validation for required fields, which rejects empty strings. JSON Schema considers `""` a valid string.
119
+
120
+ ### 4. Array Type Validation
121
+
122
+ Array element types are not strictly validated at runtime.
123
+
124
+ ### 5. Null Type
125
+
126
+ The `null` type is not fully implemented as a standalone type.
127
+
128
+ ### 6. uniqueItems
129
+
130
+ Array uniqueness constraint is not enforced during validation.
131
+
132
+ ## Workflow for Improvements
133
+
134
+ 1. **Select a Feature**: Pick a specific file from `KNOWN_FAILURES` or analyze failing tests.
135
+ 2. **Enable Tests**: Remove the file from `KNOWN_FAILURES` in the spec.
136
+ 3. **Run & Analyze**:
137
+ ```bash
138
+ bundle exec rspec --tag json_schema_compliance spec/integration/json_schema_compliance_spec.rb
139
+ ```
140
+ 4. **Implement Fix**: Modify EasyTalk internals to support the feature.
141
+ 5. **Update Converter**: If needed, update `JsonSchemaConverter` for test adaptation.
142
+
143
+ ## Critical Implementation Notes
144
+
145
+ ### Reserved Words
146
+
147
+ Properties like `method`, `class`, `constructor` conflict with Ruby. The converter sanitizes these via `sanitize_property_name` and uses the `as:` option to preserve the original JSON key.
148
+
149
+ ### Boolean Schemas
150
+
151
+ `properties: { foo: false }` is valid JSON Schema (property forbidden) but not currently supported.
152
+
153
+ ### Strict Property Validation
154
+
155
+ EasyTalk raises `InvalidPropertyNameError` for invalid property names at definition time. Full compliance would require allowing arbitrary property keys.
156
+
157
+ ## Contributing
158
+
159
+ When adding support for a new JSON Schema keyword:
160
+
161
+ 1. Check if a test file exists in `spec/fixtures/json_schema_test_suite/tests/draft7/`.
162
+ 2. Remove the filename from `KNOWN_FAILURES` in `spec/integration/json_schema_compliance_spec.rb`.
163
+ 3. Run the tests and analyze failures.
164
+ 4. Implement the feature in EasyTalk.
165
+ 5. Update this document with any new findings.
166
+
167
+ ## Related Issues
168
+
169
+ - [#137](https://github.com/sergiobayona/easy_talk/issues/137) - Add `strict_types` configuration option
@@ -0,0 +1,216 @@
1
+ ---
2
+ layout: page
3
+ title: Nested Models
4
+ permalink: /nested-models/
5
+ ---
6
+
7
+ # Nested Models
8
+
9
+ EasyTalk supports composing complex schemas from simpler building blocks. This enables clean, reusable data structures.
10
+
11
+ ## Basic Nesting
12
+
13
+ Reference another EasyTalk model as a property type:
14
+
15
+ ```ruby
16
+ class Address
17
+ include EasyTalk::Model
18
+
19
+ define_schema do
20
+ property :street, String
21
+ property :city, String
22
+ property :zip_code, String, pattern: /^\d{5}$/
23
+ end
24
+ end
25
+
26
+ class Person
27
+ include EasyTalk::Model
28
+
29
+ define_schema do
30
+ property :name, String
31
+ property :home_address, Address
32
+ property :work_address, Address, optional: true
33
+ end
34
+ end
35
+ ```
36
+
37
+ ### Generated Schema
38
+
39
+ ```json
40
+ {
41
+ "type": "object",
42
+ "properties": {
43
+ "name": { "type": "string" },
44
+ "home_address": {
45
+ "type": "object",
46
+ "properties": {
47
+ "street": { "type": "string" },
48
+ "city": { "type": "string" },
49
+ "zip_code": { "type": "string", "pattern": "^\\d{5}$" }
50
+ },
51
+ "required": ["street", "city", "zip_code"]
52
+ },
53
+ "work_address": { ... }
54
+ },
55
+ "required": ["name", "home_address"]
56
+ }
57
+ ```
58
+
59
+ ## Arrays of Models
60
+
61
+ Use `T::Array[ModelClass]` for collections:
62
+
63
+ ```ruby
64
+ class Order
65
+ include EasyTalk::Model
66
+
67
+ define_schema do
68
+ property :id, String
69
+ property :items, T::Array[LineItem], min_items: 1
70
+ property :shipping_address, Address
71
+ end
72
+ end
73
+
74
+ class LineItem
75
+ include EasyTalk::Model
76
+
77
+ define_schema do
78
+ property :product_id, String
79
+ property :quantity, Integer, minimum: 1
80
+ property :price, Float, minimum: 0
81
+ end
82
+ end
83
+ ```
84
+
85
+ ## Auto-Instantiation
86
+
87
+ When you pass a Hash to a nested model property, EasyTalk automatically instantiates the nested model:
88
+
89
+ ```ruby
90
+ person = Person.new(
91
+ name: "Alice",
92
+ home_address: {
93
+ street: "123 Main St",
94
+ city: "Boston",
95
+ zip_code: "02101"
96
+ }
97
+ )
98
+
99
+ person.home_address.class # => Address
100
+ person.home_address.city # => "Boston"
101
+ ```
102
+
103
+ This works recursively for deeply nested structures.
104
+
105
+ ## Using $ref for Reusability
106
+
107
+ By default, nested models are inlined. Enable `$ref` for cleaner, more reusable schemas:
108
+
109
+ ```ruby
110
+ class Person
111
+ include EasyTalk::Model
112
+
113
+ define_schema do
114
+ property :name, String
115
+ property :address, Address, ref: true
116
+ end
117
+ end
118
+ ```
119
+
120
+ This generates:
121
+
122
+ ```json
123
+ {
124
+ "type": "object",
125
+ "properties": {
126
+ "name": { "type": "string" },
127
+ "address": { "$ref": "#/$defs/Address" }
128
+ },
129
+ "$defs": {
130
+ "Address": {
131
+ "type": "object",
132
+ "properties": { ... }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ## Nullable Nested Models
139
+
140
+ Allow null values for nested models:
141
+
142
+ ```ruby
143
+ property :backup_address, T.nilable(Address)
144
+ ```
145
+
146
+ Or make it both nullable and optional:
147
+
148
+ ```ruby
149
+ property :backup_address, T.nilable(Address), optional: true
150
+ ```
151
+
152
+ ## Composition with compose
153
+
154
+ Use `compose` to merge multiple schemas into one:
155
+
156
+ ```ruby
157
+ class BasicInfo
158
+ include EasyTalk::Model
159
+ define_schema do
160
+ property :name, String
161
+ property :email, String
162
+ end
163
+ end
164
+
165
+ class ContactInfo
166
+ include EasyTalk::Model
167
+ define_schema do
168
+ property :phone, String
169
+ property :address, Address
170
+ end
171
+ end
172
+
173
+ class FullProfile
174
+ include EasyTalk::Model
175
+ define_schema do
176
+ compose T::AllOf[BasicInfo, ContactInfo]
177
+ end
178
+ end
179
+ ```
180
+
181
+ ## Polymorphic Types
182
+
183
+ Use `T::OneOf` or `T::AnyOf` for polymorphic properties:
184
+
185
+ ```ruby
186
+ class EmailContact
187
+ include EasyTalk::Model
188
+ define_schema do
189
+ property :email, String, format: "email"
190
+ end
191
+ end
192
+
193
+ class PhoneContact
194
+ include EasyTalk::Model
195
+ define_schema do
196
+ property :phone, String
197
+ end
198
+ end
199
+
200
+ class User
201
+ include EasyTalk::Model
202
+ define_schema do
203
+ property :name, String
204
+ property :primary_contact, T::OneOf[EmailContact, PhoneContact]
205
+ end
206
+ end
207
+ ```
208
+
209
+ This ensures the `primary_contact` matches exactly one of the specified schemas.
210
+
211
+ ## Best Practices
212
+
213
+ 1. **Keep models focused** - Each model should represent one concept
214
+ 2. **Reuse models** - Define common structures (Address, Money, etc.) once
215
+ 3. **Use $ref for large schemas** - Reduces duplication and improves readability
216
+ 4. **Validate at boundaries** - Nested models validate automatically when the parent validates