easy_talk 3.0.0 → 3.2.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +105 -0
  5. data/README.md +1268 -40
  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 +55 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/property-types.markdown +212 -0
  14. data/docs/schema-definition.markdown +180 -0
  15. data/lib/easy_talk/builders/base_builder.rb +4 -2
  16. data/lib/easy_talk/builders/composition_builder.rb +10 -12
  17. data/lib/easy_talk/builders/object_builder.rb +119 -10
  18. data/lib/easy_talk/builders/registry.rb +168 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +20 -6
  20. data/lib/easy_talk/configuration.rb +51 -1
  21. data/lib/easy_talk/error_formatter/base.rb +100 -0
  22. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  23. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  24. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  25. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  26. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  27. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  28. data/lib/easy_talk/error_formatter.rb +143 -0
  29. data/lib/easy_talk/errors.rb +2 -0
  30. data/lib/easy_talk/errors_helper.rb +63 -34
  31. data/lib/easy_talk/keywords.rb +2 -0
  32. data/lib/easy_talk/model.rb +125 -41
  33. data/lib/easy_talk/model_helper.rb +13 -0
  34. data/lib/easy_talk/naming_strategies.rb +20 -0
  35. data/lib/easy_talk/property.rb +32 -44
  36. data/lib/easy_talk/ref_helper.rb +27 -0
  37. data/lib/easy_talk/schema.rb +198 -0
  38. data/lib/easy_talk/schema_definition.rb +7 -1
  39. data/lib/easy_talk/schema_methods.rb +80 -0
  40. data/lib/easy_talk/tools/function_builder.rb +1 -1
  41. data/lib/easy_talk/type_introspection.rb +178 -0
  42. data/lib/easy_talk/types/base_composer.rb +2 -1
  43. data/lib/easy_talk/types/composer.rb +4 -0
  44. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
  45. data/lib/easy_talk/validation_adapters/base.rb +144 -0
  46. data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
  47. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  48. data/lib/easy_talk/validation_builder.rb +28 -309
  49. data/lib/easy_talk/version.rb +1 -1
  50. data/lib/easy_talk.rb +41 -0
  51. metadata +28 -6
  52. data/docs/404.html +0 -25
  53. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  54. data/easy_talk.gemspec +0 -39
data/README.md CHANGED
@@ -2,20 +2,179 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/easy_talk.svg)](https://badge.fury.io/rb/easy_talk)
4
4
  [![Ruby](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml/badge.svg)](https://github.com/sergiobayona/easy_talk/actions/workflows/dev-build.yml)
5
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/easy_talk)
6
+
7
+ ## Table of Contents
8
+
9
+ - [Introduction](#introduction)
10
+ - [What is EasyTalk?](#what-is-easytalk)
11
+ - [Key Features](#key-features)
12
+ - [Use Cases](#use-cases)
13
+ - [Inspiration](#inspiration)
14
+ - [Installation](#installation)
15
+ - [Requirements](#requirements)
16
+ - [Version 2.0.0 Breaking Changes](#version-200-breaking-changes)
17
+ - [Installation Steps](#installation-steps)
18
+ - [Verification](#verification)
19
+ - [Quick Start](#quick-start)
20
+ - [Minimal Example](#minimal-example)
21
+ - [Generated JSON Schema](#generated-json-schema)
22
+ - [Basic Usage](#basic-usage)
23
+ - [Core Concepts](#core-concepts)
24
+ - [Schema Definition](#schema-definition)
25
+ - [Property Types](#property-types)
26
+ - [Ruby Types](#ruby-types)
27
+ - [Sorbet-Style Types](#sorbet-style-types)
28
+ - [Custom Types](#custom-types)
29
+ - [Property Constraints](#property-constraints)
30
+ - [Required vs Optional Properties](#required-vs-optional-properties)
31
+ - [Automatic Validation Generation](#automatic-validation-generation)
32
+ - [Manual Validation Overrides](#manual-validation-overrides)
33
+ - [Defining Schemas](#defining-schemas)
34
+ - [Basic Schema Structure](#basic-schema-structure)
35
+ - [Property Definitions](#property-definitions)
36
+ - [Arrays and Collections](#arrays-and-collections)
37
+ - [Constraints and Automatic Validations](#constraints-and-automatic-validations)
38
+ - [Supported Constraint-to-Validation Mappings](#supported-constraint-to-validation-mappings)
39
+ - [Additional Properties](#additional-properties)
40
+ - [Property Naming](#property-naming)
41
+ - [Schema Composition](#schema-composition)
42
+ - [Using T::AnyOf](#using-tanyof)
43
+ - [Using T::OneOf](#using-toneof)
44
+ - [Using T::AllOf](#using-tallof)
45
+ - [Array Composition](#array-composition)
46
+ - [Complex Compositions](#complex-compositions)
47
+ - [Reusing Models](#reusing-models)
48
+ - [ActiveModel Integration](#activemodel-integration)
49
+ - [Enhanced Validation System](#enhanced-validation-system)
50
+ - [Error Handling](#error-handling)
51
+ - [Standardized Error Formatting](#standardized-error-formatting)
52
+ - [Available Formats](#available-formats)
53
+ - [Instance Methods](#instance-methods)
54
+ - [Direct API Usage](#direct-api-usage)
55
+ - [Configuration](#configuration)
56
+ - [Model Attributes](#model-attributes)
57
+ - [Advanced Features](#advanced-features)
58
+ - [Schema-Only Mode (EasyTalk::Schema)](#schema-only-mode-easytalkschema)
59
+ - [Key Differences from EasyTalk::Model](#key-differences-from-easytalkmodel)
60
+ - [When to Use Each](#when-to-use-each)
61
+ - [LLM Function Generation](#llm-function-generation)
62
+ - [Schema Transformation](#schema-transformation)
63
+ - [Type Checking and Validation](#type-checking-and-validation)
64
+ - [Custom Type Builders](#custom-type-builders)
65
+ - [Registering Custom Types](#registering-custom-types)
66
+ - [Creating a Custom Builder](#creating-a-custom-builder)
67
+ - [Collection Type Builders](#collection-type-builders)
68
+ - [Overriding Built-in Types](#overriding-built-in-types)
69
+ - [Registry API](#registry-api)
70
+ - [Type Introspection](#type-introspection)
71
+ - [Configuration](#configuration-1)
72
+ - [Global Settings](#global-settings)
73
+ - [Automatic Validation Configuration](#automatic-validation-configuration)
74
+ - [Per-Model Configuration](#per-model-configuration)
75
+ - [Validation Adapters](#validation-adapters)
76
+ - [Built-in Adapters](#built-in-adapters)
77
+ - [Global Adapter Configuration](#global-adapter-configuration)
78
+ - [Per-Model Validation Control](#per-model-validation-control)
79
+ - [Per-Property Validation Control](#per-property-validation-control)
80
+ - [Custom Validation Adapters](#custom-validation-adapters)
81
+ - [Examples](#examples)
82
+ - [User Registration (with Auto-Validations)](#user-registration-with-auto-validations)
83
+ - [Payment Processing](#payment-processing)
84
+ - [Complex Object Hierarchies](#complex-object-hierarchies)
85
+ - [API Integration](#api-integration)
86
+ - [Troubleshooting](#troubleshooting)
87
+ - [Common Errors](#common-errors)
88
+ - ["Invalid property name"](#invalid-property-name)
89
+ - ["Property type is missing"](#property-type-is-missing)
90
+ - ["Unknown option"](#unknown-option)
91
+ - [Schema Validation Issues](#schema-validation-issues)
92
+ - [Type Errors](#type-errors)
93
+ - [Best Practices](#best-practices)
94
+ - [Nullable vs Optional Properties in EasyTalk](#nullable-vs-optional-properties-in-easytalk)
95
+ - [Key Concepts](#key-concepts)
96
+ - [Nullable Properties](#nullable-properties)
97
+ - [Optional Properties](#optional-properties)
98
+ - [Nullable AND Optional Properties](#nullable-and-optional-properties)
99
+ - [Configuration Options](#configuration-options)
100
+ - [Practical Examples](#practical-examples)
101
+ - [User Profile Schema](#user-profile-schema)
102
+ - [Common Gotchas](#common-gotchas)
103
+ - [Misconception: Nullable Implies Optional](#misconception-nullable-implies-optional)
104
+ - [Misconception: Optional Properties Accept Null](#misconception-optional-properties-accept-null)
105
+ - [Migration from Earlier Versions](#migration-from-earlier-versions)
106
+ - [Best Practices](#best-practices-1)
107
+ - [JSON Schema Comparison](#json-schema-comparison)
108
+ - [Migration Guide from v1.x to v2.0](#migration-guide-from-v1x-to-v20)
109
+ - [Breaking Changes Summary](#breaking-changes-summary)
110
+ - [Migration Steps](#migration-steps)
111
+ - [1. Replace Hash-based Nested Schemas](#1-replace-hash-based-nested-schemas)
112
+ - [2. Review Automatic Validations](#2-review-automatic-validations)
113
+ - [3. Configuration Updates](#3-configuration-updates)
114
+ - [Compatibility Notes](#compatibility-notes)
115
+ - [Development and Contributing](#development-and-contributing)
116
+ - [Setting Up the Development Environment](#setting-up-the-development-environment)
117
+ - [Running Tests](#running-tests)
118
+ - [Code Quality](#code-quality)
119
+ - [Contributing Guidelines](#contributing-guidelines)
120
+ - [JSON Schema Version (`$schema` Keyword)](#json-schema-version-schema-keyword)
121
+ - [Why Use `$schema`?](#why-use-schema)
122
+ - [Supported Draft Versions](#supported-draft-versions)
123
+ - [Global Configuration](#global-configuration)
124
+ - [Per-Model Configuration](#per-model-configuration-1)
125
+ - [Disabling `$schema` for Specific Models](#disabling-schema-for-specific-models)
126
+ - [Custom Schema URIs](#custom-schema-uris)
127
+ - [Nested Models](#nested-models)
128
+ - [Default Behavior](#default-behavior)
129
+ - [Best Practices](#best-practices-2)
130
+ - [Schema Identifier (`$id` Keyword)](#schema-identifier-id-keyword)
131
+ - [Why Use `$id`?](#why-use-id)
132
+ - [Global Configuration](#global-configuration-1)
133
+ - [Per-Model Configuration](#per-model-configuration-2)
134
+ - [Disabling `$id` for Specific Models](#disabling-id-for-specific-models)
135
+ - [Combining `$schema` and `$id`](#combining-schema-and-id)
136
+ - [Nested Models](#nested-models-1)
137
+ - [URI Formats](#uri-formats)
138
+ - [Default Behavior](#default-behavior-1)
139
+ - [Best Practices](#best-practices-3)
140
+ - [Schema References (`$ref` and `$defs`)](#schema-references-ref-and-defs)
141
+ - [Why Use `$ref`?](#why-use-ref)
142
+ - [Default Behavior (Inline Schemas)](#default-behavior-inline-schemas)
143
+ - [Enabling `$ref` References](#enabling-ref-references)
144
+ - [Global Configuration](#global-configuration-2)
145
+ - [Per-Property Configuration](#per-property-configuration)
146
+ - [Arrays of Models](#arrays-of-models)
147
+ - [Nilable Models with `$ref`](#nilable-models-with-ref)
148
+ - [Multiple References to the Same Model](#multiple-references-to-the-same-model)
149
+ - [Combining `$ref` with Other Constraints](#combining-ref-with-other-constraints)
150
+ - [Interaction with `compose`](#interaction-with-compose)
151
+ - [Best Practices](#best-practices-4)
152
+ - [Default Behavior](#default-behavior-2)
153
+ - [JSON Schema Compatibility](#json-schema-compatibility)
154
+ - [Supported Versions](#supported-versions)
155
+ - [Specification Compliance](#specification-compliance)
156
+ - [Known Limitations](#known-limitations)
157
+ - [API Reference](#api-reference)
158
+ - [Core Modules](#core-modules)
159
+ - [Builders](#builders)
160
+ - [Configuration & Utilities](#configuration--utilities)
161
+ - [License](#license)
5
162
 
6
163
  ## Introduction
7
164
 
8
165
  ### What is EasyTalk?
9
- EasyTalk is a Ruby library that simplifies defining and generating JSON Schema. It provides an intuitive interface for Ruby developers to define structured data models that can be used for validation and documentation.
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.
10
167
 
11
168
  ### Key Features
12
- * **Intuitive Schema Definition**: Use Ruby classes and methods to define JSON Schema documents easily.
13
- * **Automatic ActiveModel Validations**: Schema constraints automatically generate corresponding ActiveModel validations (configurable).
14
- * **Works for plain Ruby classes and ActiveModel classes**: Integrate with existing code or build from scratch.
15
- * **LLM Function Support**: Ideal for integrating with Large Language Models (LLMs) such as OpenAI's GPT series. EasyTalk enables you to effortlessly create JSON Schema documents describing the inputs and outputs of LLM function calls.
16
- * **Schema Composition**: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
17
- * **Enhanced Model Integration**: Automatic instantiation of nested EasyTalk models from hash attributes.
18
- * **Flexible Configuration**: Global and per-model configuration options for fine-tuned control.
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.
19
178
 
20
179
  ### Use Cases
21
180
  - API request/response validation
@@ -25,7 +184,7 @@ EasyTalk is a Ruby library that simplifies defining and generating JSON Schema.
25
184
  - Configuration schema definitions
26
185
 
27
186
  ### Inspiration
28
- Inspired by Python's Pydantic library, EasyTalk brings similar functionality to the Ruby ecosystem, providing a Ruby-friendly approach to JSON Schema operations.
187
+ Inspired by Python's [Pydantic](https://docs.pydantic.dev/) library, EasyTalk brings similar functionality to the Ruby ecosystem, providing a Ruby-friendly approach to JSON Schema operations.
29
188
 
30
189
  ## Installation
31
190
 
@@ -364,6 +523,27 @@ company.location = "New York" # Additional property
364
523
  company.employee_count = 100 # Additional property
365
524
  ```
366
525
 
526
+ ### Property Naming
527
+ You can configure the naming strategy for properties globally or per schema:
528
+
529
+ ```ruby
530
+ EasyTalk.configure do |config|
531
+ config.property_naming_strategy = :snake_case # Options: :identity, :snake_case, :camel_case, :pascal_case
532
+ end
533
+
534
+ define_schema do
535
+ property_naming_strategy :camel_case # Overrides global setting for this schema
536
+ property :name, String
537
+ end
538
+ ```
539
+
540
+ This affects how property names are represented in the generated JSON Schema.
541
+ Additionally, names can be overridden per property:
542
+
543
+ ```ruby
544
+ property :first_name, String, as: "firstName" # Overrides global naming strategy
545
+ ```
546
+
367
547
  ## Schema Composition
368
548
 
369
549
  ### Using T::AnyOf
@@ -405,13 +585,67 @@ class VehicleRegistration
405
585
  end
406
586
  ```
407
587
 
588
+ ### Array Composition
589
+ Composition types can be combined with arrays to define collections where each item must match one of several schemas:
590
+
591
+ ```ruby
592
+ class ProductA
593
+ include EasyTalk::Model
594
+ define_schema do
595
+ property :sku, String
596
+ property :weight, Float
597
+ end
598
+ end
599
+
600
+ class ProductB
601
+ include EasyTalk::Model
602
+ define_schema do
603
+ property :sku, String
604
+ property :digital_url, String
605
+ end
606
+ end
607
+
608
+ class Order
609
+ include EasyTalk::Model
610
+
611
+ define_schema do
612
+ property :order_id, String
613
+ # Each item in the array must match exactly one of the product schemas
614
+ property :items, T::Array[T::OneOf[ProductA, ProductB]]
615
+ end
616
+ end
617
+ ```
618
+
619
+ This generates a JSON Schema where the `items` array validates each element against `oneOf`:
620
+
621
+ ```json
622
+ {
623
+ "properties": {
624
+ "items": {
625
+ "type": "array",
626
+ "items": {
627
+ "oneOf": [
628
+ { "type": "object", "properties": { "sku": {...}, "weight": {...} }, ... },
629
+ { "type": "object", "properties": { "sku": {...}, "digital_url": {...} }, ... }
630
+ ]
631
+ }
632
+ }
633
+ }
634
+ }
635
+ ```
636
+
637
+ You can use any composition type with arrays:
638
+ - `T::Array[T::OneOf[A, B]]` - each item matches exactly one schema
639
+ - `T::Array[T::AnyOf[A, B]]` - each item matches one or more schemas
640
+ - `T::Array[T::AllOf[A, B]]` - each item matches all schemas
641
+
408
642
  ### Complex Compositions
409
643
  You can combine composition types to create complex schemas:
410
644
 
411
645
  ```ruby
412
646
  class ComplexObject
413
647
  include EasyTalk::Model
414
-
648
+
415
649
  define_schema do
416
650
  property :basic_info, BaseInfo
417
651
  property :specific_details, T::OneOf[DetailTypeA, DetailTypeB]
@@ -477,6 +711,78 @@ user.errors[:age] # => ["must be greater than 21"] # Custom validation
477
711
  user.errors[:height] # => ["must be greater than 0"] # Overridden validation
478
712
  ```
479
713
 
714
+ ### Standardized Error Formatting
715
+
716
+ EasyTalk provides multiple output formats for validation errors, making it easy to build consistent API responses.
717
+
718
+ #### Available Formats
719
+
720
+ | Format | Description | Use Case |
721
+ |--------|-------------|----------|
722
+ | `:flat` | Simple array of field/message/code objects | General purpose APIs |
723
+ | `:json_pointer` | Array with JSON Pointer (RFC 6901) paths | JSON Schema validation |
724
+ | `:rfc7807` | RFC 7807 Problem Details format | Standards-compliant APIs |
725
+ | `:jsonapi` | JSON:API specification error format | JSON:API implementations |
726
+
727
+ #### Instance Methods
728
+
729
+ Every EasyTalk model includes convenient methods for error formatting:
730
+
731
+ ```ruby
732
+ user = User.new(name: "", email: "invalid")
733
+ user.valid?
734
+
735
+ # Use default format (configurable globally)
736
+ user.validation_errors
737
+ # => [{"field" => "name", "message" => "can't be blank", "code" => "blank"}, ...]
738
+
739
+ # Flat format
740
+ user.validation_errors_flat
741
+ # => [{"field" => "name", "message" => "can't be blank", "code" => "blank"}]
742
+
743
+ # JSON Pointer format
744
+ user.validation_errors_json_pointer
745
+ # => [{"pointer" => "/properties/name", "message" => "can't be blank", "code" => "blank"}]
746
+
747
+ # RFC 7807 Problem Details
748
+ user.validation_errors_rfc7807
749
+ # => {
750
+ # "type" => "about:blank#validation-error",
751
+ # "title" => "Validation Failed",
752
+ # "status" => 422,
753
+ # "detail" => "The request contains invalid parameters",
754
+ # "errors" => [...]
755
+ # }
756
+
757
+ # JSON:API format
758
+ user.validation_errors_jsonapi
759
+ # => {
760
+ # "errors" => [
761
+ # {"status" => "422", "source" => {"pointer" => "/data/attributes/name"}, ...}
762
+ # ]
763
+ # }
764
+ ```
765
+
766
+ #### Direct API Usage
767
+
768
+ You can also format errors directly using the `ErrorFormatter` module:
769
+
770
+ ```ruby
771
+ EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: "User Validation Failed")
772
+ ```
773
+
774
+ #### Configuration
775
+
776
+ Configure error formatting globally:
777
+
778
+ ```ruby
779
+ EasyTalk.configure do |config|
780
+ config.default_error_format = :rfc7807 # Default format for validation_errors
781
+ config.error_type_base_uri = 'https://api.example.com/errors' # Base URI for RFC 7807
782
+ config.include_error_codes = true # Include error codes in output
783
+ end
784
+ ```
785
+
480
786
  ### Model Attributes
481
787
  EasyTalk models provide getters and setters for all defined properties:
482
788
 
@@ -520,6 +826,57 @@ user.address.street # => "123 Main St"
520
826
 
521
827
  ## Advanced Features
522
828
 
829
+ ### Schema-Only Mode (EasyTalk::Schema)
830
+
831
+ For scenarios where you need JSON Schema generation without ActiveModel validations, use `EasyTalk::Schema` instead of `EasyTalk::Model`. This is ideal for:
832
+
833
+ - API documentation and OpenAPI spec generation
834
+ - Schema-first design where validation happens elsewhere
835
+ - High-performance scenarios where validation overhead is unwanted
836
+ - Generating schemas for external systems
837
+
838
+ ```ruby
839
+ class ApiContract
840
+ include EasyTalk::Schema # Not EasyTalk::Model
841
+
842
+ define_schema do
843
+ title 'API Contract'
844
+ description 'A schema-only contract'
845
+ property :name, String, min_length: 2
846
+ property :age, Integer, minimum: 0
847
+ end
848
+ end
849
+
850
+ # Schema generation works the same
851
+ ApiContract.json_schema
852
+ # => {"type" => "object", "title" => "API Contract", ...}
853
+
854
+ # Instances can be created and accessed
855
+ contract = ApiContract.new(name: 'Test', age: 25)
856
+ contract.name # => 'Test'
857
+
858
+ # But no validation methods are available
859
+ contract.valid? # => NoMethodError
860
+ contract.errors # => NoMethodError
861
+ ```
862
+
863
+ #### Key Differences from EasyTalk::Model
864
+
865
+ | Feature | EasyTalk::Model | EasyTalk::Schema |
866
+ |---------|-----------------|------------------|
867
+ | JSON Schema generation | Yes | Yes |
868
+ | Property accessors | Yes | Yes |
869
+ | Nested model instantiation | Yes | Yes |
870
+ | ActiveModel::Validations | Yes | No |
871
+ | `valid?` / `errors` | Yes | No |
872
+ | Validation adapters | Yes | N/A |
873
+ | Error formatting | Yes | No |
874
+
875
+ #### When to Use Each
876
+
877
+ - **Use `EasyTalk::Model`** when you need runtime validation of data (form inputs, API requests, user data)
878
+ - **Use `EasyTalk::Schema`** when you only need the schema definition (documentation, code generation, external validation)
879
+
523
880
  ### LLM Function Generation
524
881
  EasyTalk provides a helper method for generating OpenAI function specifications:
525
882
 
@@ -566,16 +923,125 @@ property :age, Integer, enum: ["young", "old"] # Error!
566
923
  ```
567
924
 
568
925
  ### Custom Type Builders
569
- For advanced use cases, you can create custom type builders:
926
+
927
+ EasyTalk provides a type registry that allows you to register custom types with their corresponding schema builders.
928
+
929
+ #### Registering Custom Types
930
+
931
+ Register types in your configuration:
570
932
 
571
933
  ```ruby
572
- module EasyTalk
573
- module Builders
574
- class MyCustomTypeBuilder < BaseBuilder
575
- # Custom implementation
576
- end
934
+ EasyTalk.configure do |config|
935
+ config.register_type(Money, MoneySchemaBuilder)
936
+ end
937
+ ```
938
+
939
+ Or register directly with the registry:
940
+
941
+ ```ruby
942
+ EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
943
+ ```
944
+
945
+ #### Creating a Custom Builder
946
+
947
+ Custom builders extend `BaseBuilder` and implement the schema generation logic:
948
+
949
+ ```ruby
950
+ class MoneySchemaBuilder < EasyTalk::Builders::BaseBuilder
951
+ VALID_OPTIONS = {
952
+ currency: { type: T.nilable(String), key: :currency }
953
+ }.freeze
954
+
955
+ def initialize(name, options = {})
956
+ super(name, { type: 'object' }, options, VALID_OPTIONS)
957
+ end
958
+
959
+ def build
960
+ schema.merge(
961
+ properties: {
962
+ amount: { type: 'number' },
963
+ currency: { type: 'string', default: options[:currency] || 'USD' }
964
+ },
965
+ required: %w[amount currency]
966
+ )
967
+ end
968
+ end
969
+
970
+ # Register and use
971
+ EasyTalk::Builders::Registry.register(Money, MoneySchemaBuilder)
972
+
973
+ class Order
974
+ include EasyTalk::Model
975
+
976
+ define_schema do
977
+ property :total, Money, currency: 'EUR'
978
+ end
979
+ end
980
+ ```
981
+
982
+ #### Collection Type Builders
983
+
984
+ For types that wrap other types (like arrays), use the `collection: true` option:
985
+
986
+ ```ruby
987
+ EasyTalk::Builders::Registry.register(
988
+ CustomCollection,
989
+ CustomCollectionBuilder,
990
+ collection: true
991
+ )
992
+ ```
993
+
994
+ Collection builders receive `(name, inner_type, constraints)` instead of `(name, constraints)`.
995
+
996
+ #### Overriding Built-in Types
997
+
998
+ You can override built-in type builders:
999
+
1000
+ ```ruby
1001
+ class EnhancedStringBuilder < EasyTalk::Builders::StringBuilder
1002
+ def build
1003
+ result = super
1004
+ result[:custom_extension] = true
1005
+ result
577
1006
  end
578
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
579
1045
  ```
580
1046
 
581
1047
  ## Configuration
@@ -588,7 +1054,20 @@ EasyTalk.configure do |config|
588
1054
  # Schema behavior options
589
1055
  config.default_additional_properties = false # Control additional properties on all models
590
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
591
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
592
1071
  end
593
1072
  ```
594
1073
 
@@ -621,7 +1100,7 @@ You can configure additional properties for individual models:
621
1100
  ```ruby
622
1101
  class User
623
1102
  include EasyTalk::Model
624
-
1103
+
625
1104
  define_schema do
626
1105
  title "User"
627
1106
  additional_properties true # Allow arbitrary additional properties on this model
@@ -631,6 +1110,74 @@ class User
631
1110
  end
632
1111
  ```
633
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
+
634
1181
  ## Examples
635
1182
 
636
1183
  ### User Registration (with Auto-Validations)
@@ -871,18 +1418,18 @@ property :status, String, enum: ["active", "inactive", "pending"]
871
1418
  9. **Use nullable_optional_property** for fields that can be omitted or null
872
1419
  10. **Document breaking changes** when updating schema definitions
873
1420
 
874
- # Nullable vs Optional Properties in EasyTalk
1421
+ ## Nullable vs Optional Properties in EasyTalk
875
1422
 
876
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.
877
1424
 
878
- ## Key Concepts
1425
+ ### Key Concepts
879
1426
 
880
1427
  | Concept | Description | JSON Schema Effect | EasyTalk Syntax |
881
1428
  |---------|-------------|-------------------|-----------------|
882
1429
  | **Nullable** | Property can have a `null` value | Adds `"null"` to the type array | `T.nilable(Type)` |
883
1430
  | **Optional** | Property doesn't have to exist | Omits property from `"required"` array | `optional: true` constraint |
884
1431
 
885
- ## Nullable Properties
1432
+ ### Nullable Properties
886
1433
 
887
1434
  A **nullable** property can contain a `null` value, but the property itself must still be present in the object:
888
1435
 
@@ -908,7 +1455,7 @@ In this case, the following data would be valid:
908
1455
  But this would be invalid:
909
1456
  - `{ }` (missing the age property entirely)
910
1457
 
911
- ## Optional Properties
1458
+ ### Optional Properties
912
1459
 
913
1460
  An **optional** property doesn't have to be present in the object at all:
914
1461
 
@@ -934,7 +1481,7 @@ In this case, the following data would be valid:
934
1481
  But this would be invalid:
935
1482
  - `{ "nickname": null }` (null is not allowed because the property isn't nullable)
936
1483
 
937
- ## Nullable AND Optional Properties
1484
+ ### Nullable AND Optional Properties
938
1485
 
939
1486
  For properties that should be both nullable and optional (can be omitted or null), you need to combine both approaches:
940
1487
 
@@ -961,7 +1508,7 @@ nullable_optional_property :bio, String
961
1508
 
962
1509
  Which is equivalent to the above.
963
1510
 
964
- ## Configuration Options
1511
+ ### Configuration Options
965
1512
 
966
1513
  By default, nullable properties are still required. You can change this global behavior:
967
1514
 
@@ -973,9 +1520,9 @@ end
973
1520
 
974
1521
  With this configuration, any property defined with `T.nilable(Type)` will be treated as both nullable and optional.
975
1522
 
976
- ## Practical Examples
1523
+ ### Practical Examples
977
1524
 
978
- ### User Profile Schema
1525
+ #### User Profile Schema
979
1526
 
980
1527
  ```ruby
981
1528
  class UserProfile
@@ -1004,17 +1551,17 @@ This creates clear expectations for data validation:
1004
1551
  - `email` doesn't have to be present, but if it is, it cannot be null
1005
1552
  - `bio` doesn't have to be present, and if it is, it can be null
1006
1553
 
1007
- ## Common Gotchas
1554
+ ### Common Gotchas
1008
1555
 
1009
- ### Misconception: Nullable Implies Optional
1556
+ #### Misconception: Nullable Implies Optional
1010
1557
 
1011
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.
1012
1559
 
1013
- ### Misconception: Optional Properties Accept Null
1560
+ #### Misconception: Optional Properties Accept Null
1014
1561
 
1015
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)`.
1016
1563
 
1017
- ## Migration from Earlier Versions
1564
+ ### Migration from Earlier Versions
1018
1565
 
1019
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.
1020
1567
 
@@ -1028,14 +1575,14 @@ end
1028
1575
 
1029
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.
1030
1577
 
1031
- ## Best Practices
1578
+ ### Best Practices
1032
1579
 
1033
1580
  1. **Be explicit about intent**: Always clarify whether properties should be nullable, optional, or both
1034
1581
  2. **Use the helper method**: For properties that are both nullable and optional, use `nullable_optional_property`
1035
1582
  3. **Document expectations**: Use comments to clarify validation requirements for complex schemas
1036
1583
  4. **Consider validation implications**: Remember that ActiveModel validations operate independently of the schema definition
1037
1584
 
1038
- ## JSON Schema Comparison
1585
+ ### JSON Schema Comparison
1039
1586
 
1040
1587
  | EasyTalk Definition | Required | Nullable | JSON Schema Equivalent |
1041
1588
  |--------------------|----------|----------|------------------------|
@@ -1168,18 +1715,699 @@ bundle exec rubocop
1168
1715
  ### Contributing Guidelines
1169
1716
  Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
1170
1717
 
1171
- ## JSON Schema Compatibility
1718
+ ## JSON Schema Version (`$schema` Keyword)
1172
1719
 
1173
- ### Supported Versions
1174
- EasyTalk is currently loose about JSON Schema versions. It doesn't strictly enforce or adhere to any particular version of the specification. The goal is to add more robust support for the latest JSON Schema specs in the future.
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.
1175
1721
 
1176
- ### Specification Compliance
1177
- 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.
1722
+ ### Why Use `$schema`?
1178
1723
 
1179
- ### Known Limitations
1180
- - Limited support for custom formats
1181
- - No direct support for JSON Schema draft 2020-12 features
1182
- - Complex composition scenarios may require manual adjustment
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
1761
+ end
1762
+ end
1763
+
1764
+ User.json_schema
1765
+ # => {
1766
+ # "$schema" => "https://json-schema.org/draft/2020-12/schema",
1767
+ # "type" => "object",
1768
+ # "properties" => { "name" => { "type" => "string" } },
1769
+ # "required" => ["name"],
1770
+ # "additionalProperties" => false
1771
+ # }
1772
+ ```
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
1780
+ include EasyTalk::Model
1781
+
1782
+ define_schema do
1783
+ schema_version :draft7 # Use Draft-07 for this specific model
1784
+ property :name, String
1785
+ end
1786
+ end
1787
+
1788
+ LegacyModel.json_schema
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
1821
+ ```
1822
+
1823
+ ### Custom Schema URIs
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
1830
+
1831
+ define_schema do
1832
+ schema_version 'https://my-company.com/schemas/v1/meta-schema.json'
1833
+ property :id, String
1834
+ end
1835
+ end
1836
+ ```
1837
+
1838
+ ### Nested Models
1839
+
1840
+ The `$schema` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$schema`:
1841
+
1842
+ ```ruby
1843
+ EasyTalk.configure do |config|
1844
+ config.schema_version = :draft202012
1845
+ end
1846
+
1847
+ class Address
1848
+ include EasyTalk::Model
1849
+ define_schema do
1850
+ property :city, String
1851
+ end
1852
+ end
1853
+
1854
+ class User
1855
+ include EasyTalk::Model
1856
+ define_schema do
1857
+ property :name, String
1858
+ property :address, Address
1859
+ end
1860
+ 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
+ ```
1913
+
1914
+ With this configuration, all models will include `$id` in their output:
1915
+
1916
+ ```ruby
1917
+ class User
1918
+ include EasyTalk::Model
1919
+
1920
+ define_schema do
1921
+ property :name, String
1922
+ end
1923
+ 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
+ ```
1934
+
1935
+ ### Per-Model Configuration
1936
+
1937
+ Override the global setting for individual models using the `schema_id` keyword in the schema definition:
1938
+
1939
+ ```ruby
1940
+ class User
1941
+ include EasyTalk::Model
1942
+
1943
+ define_schema do
1944
+ schema_id 'https://example.com/schemas/user.schema.json'
1945
+ property :name, String
1946
+ property :email, String
1947
+ end
1948
+ end
1949
+
1950
+ User.json_schema
1951
+ # => {
1952
+ # "$id" => "https://example.com/schemas/user.schema.json",
1953
+ # "type" => "object",
1954
+ # ...
1955
+ # }
1956
+ ```
1957
+
1958
+ ### Disabling `$id` for Specific Models
1959
+
1960
+ If you have a global schema ID configured but want to exclude `$id` from a specific model, use `:none`:
1961
+
1962
+ ```ruby
1963
+ EasyTalk.configure do |config|
1964
+ config.schema_id = 'https://example.com/schemas/default.json'
1965
+ end
1966
+
1967
+ class InternalModel
1968
+ include EasyTalk::Model
1969
+
1970
+ define_schema do
1971
+ schema_id :none # No $id for this model
1972
+ property :data, String
1973
+ end
1974
+ end
1975
+
1976
+ InternalModel.json_schema
1977
+ # => {
1978
+ # "type" => "object",
1979
+ # "properties" => { "data" => { "type" => "string" } },
1980
+ # ...
1981
+ # }
1982
+ # Note: No "$id" key present
1983
+ ```
1984
+
1985
+ ### Combining `$schema` and `$id`
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
2000
+
2001
+ Product.json_schema
2002
+ # => {
2003
+ # "$schema" => "https://json-schema.org/draft/2020-12/schema",
2004
+ # "$id" => "https://example.com/schemas/product.schema.json",
2005
+ # "type" => "object",
2006
+ # ...
2007
+ # }
2008
+ ```
2009
+
2010
+ ### Nested Models
2011
+
2012
+ The `$id` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$id`:
2013
+
2014
+ ```ruby
2015
+ EasyTalk.configure do |config|
2016
+ config.schema_id = 'https://example.com/schemas/user.json'
2017
+ end
2018
+
2019
+ class Address
2020
+ include EasyTalk::Model
2021
+ define_schema do
2022
+ property :city, String
2023
+ end
2024
+ end
2025
+
2026
+ class User
2027
+ include EasyTalk::Model
2028
+ define_schema do
2029
+ property :name, String
2030
+ property :address, Address
2031
+ end
2032
+ 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
+ ```
2064
+
2065
+ ### Default Behavior
2066
+
2067
+ By default, `schema_id` is set to `nil`, meaning no `$id` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
2068
+
2069
+ ### Best Practices
2070
+
2071
+ 1. **Use absolute URIs for published schemas**: This ensures global uniqueness and enables external references.
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).
2078
+
2079
+ ## Schema References (`$ref` and `$defs`)
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:
2094
+
2095
+ ```ruby
2096
+ class Address
2097
+ include EasyTalk::Model
2098
+ define_schema do
2099
+ property :street, String
2100
+ property :city, String
2101
+ end
2102
+ end
2103
+
2104
+ class Person
2105
+ include EasyTalk::Model
2106
+ define_schema do
2107
+ property :name, String
2108
+ property :address, Address
2109
+ end
2110
+ end
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
2131
+
2132
+ #### Global Configuration
2133
+
2134
+ Enable `$ref` generation for all nested models:
2135
+
2136
+ ```ruby
2137
+ EasyTalk.configure do |config|
2138
+ config.use_refs = true
2139
+ end
2140
+ ```
2141
+
2142
+ With this configuration, nested models are referenced via `$ref` and their definitions are placed in `$defs`:
2143
+
2144
+ ```ruby
2145
+ Person.json_schema
2146
+ # => {
2147
+ # "type" => "object",
2148
+ # "properties" => {
2149
+ # "name" => { "type" => "string" },
2150
+ # "address" => { "$ref" => "#/$defs/Address" }
2151
+ # },
2152
+ # "$defs" => {
2153
+ # "Address" => {
2154
+ # "type" => "object",
2155
+ # "properties" => {
2156
+ # "street" => { "type" => "string" },
2157
+ # "city" => { "type" => "string" }
2158
+ # },
2159
+ # ...
2160
+ # }
2161
+ # },
2162
+ # ...
2163
+ # }
2164
+ ```
2165
+
2166
+ #### Per-Property Configuration
2167
+
2168
+ You can also enable `$ref` for specific properties using the `ref: true` constraint:
2169
+
2170
+ ```ruby
2171
+ class Person
2172
+ 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
+
2187
+ class Person
2188
+ include EasyTalk::Model
2189
+ define_schema do
2190
+ 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
+
2198
+ When using `$ref` with arrays of models, the `$ref` applies to the array items:
2199
+
2200
+ ```ruby
2201
+ EasyTalk.configure do |config|
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]
2210
+ end
2211
+ 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
+ ```
2235
+
2236
+ ### Nilable Models with `$ref`
2237
+
2238
+ When using `$ref` with nilable model types, EasyTalk uses `anyOf` to combine the reference with the null type:
2239
+
2240
+ ```ruby
2241
+ EasyTalk.configure do |config|
2242
+ config.use_refs = true
2243
+ end
2244
+
2245
+ class Person
2246
+ 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
+
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
+ define_schema do
2280
+ property :name, String
2281
+ property :home_address, Address, ref: true
2282
+ property :work_address, Address, ref: true
2283
+ property :shipping_addresses, T::Array[Address], ref: true
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
+ ```
2305
+
2306
+ ### Combining `$ref` with Other Constraints
2307
+
2308
+ You can add additional constraints alongside `$ref`:
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"
2315
+ end
2316
+ end
2317
+
2318
+ Person.json_schema["properties"]["address"]
2319
+ # => {
2320
+ # "$ref" => "#/$defs/Address",
2321
+ # "description" => "Primary address",
2322
+ # "title" => "Main Address"
2323
+ # }
2324
+ ```
2325
+
2326
+ ### Interaction with `compose`
2327
+
2328
+ When using `compose` with `T::AllOf`, `T::AnyOf`, or `T::OneOf`, the composed models are also placed in `$defs`:
2329
+
2330
+ ```ruby
2331
+ class Employee
2332
+ include EasyTalk::Model
2333
+ define_schema do
2334
+ compose T::AllOf[Person, EmployeeDetails]
2335
+ property :badge_number, String
2336
+ end
2337
+ 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
+
2352
+ ### Default Behavior
2353
+
2354
+ By default, `use_refs` is set to `false`, meaning nested models are inlined. This maintains backward compatibility with previous versions of EasyTalk.
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
2370
+
2371
+ ## API Reference
2372
+
2373
+ For complete API documentation, visit [RubyDoc.info](https://rubydoc.info/gems/easy_talk).
2374
+
2375
+ ### Core Modules
2376
+
2377
+ | Module | Description | Source |
2378
+ |--------|-------------|--------|
2379
+ | [`EasyTalk::Model`](https://rubydoc.info/gems/easy_talk/EasyTalk/Model) | Main mixin providing schema definition, validation, and JSON Schema generation | [lib/easy_talk/model.rb](lib/easy_talk/model.rb) |
2380
+ | [`EasyTalk::Schema`](https://rubydoc.info/gems/easy_talk/EasyTalk/Schema) | Schema-only mode without ActiveModel validations | [lib/easy_talk/schema.rb](lib/easy_talk/schema.rb) |
2381
+ | [`EasyTalk::SchemaDefinition`](https://rubydoc.info/gems/easy_talk/EasyTalk/SchemaDefinition) | DSL for defining properties, titles, and descriptions | [lib/easy_talk/schema_definition.rb](lib/easy_talk/schema_definition.rb) |
2382
+ | [`EasyTalk::Property`](https://rubydoc.info/gems/easy_talk/EasyTalk/Property) | Property definition and type dispatching | [lib/easy_talk/property.rb](lib/easy_talk/property.rb) |
2383
+
2384
+ ### Builders
2385
+
2386
+ Type-specific builders that convert Ruby definitions to JSON Schema. See the [builders directory](lib/easy_talk/builders/) for all available builders.
2387
+
2388
+ | Builder | Description |
2389
+ |---------|-------------|
2390
+ | [`ObjectBuilder`](lib/easy_talk/builders/object_builder.rb) | Handles EasyTalk::Model classes |
2391
+ | [`StringBuilder`](lib/easy_talk/builders/string_builder.rb) | String type with format, pattern, length constraints |
2392
+ | [`IntegerBuilder`](lib/easy_talk/builders/integer_builder.rb) | Integer type with min/max constraints |
2393
+ | [`NumberBuilder`](lib/easy_talk/builders/number_builder.rb) | Float/Number type with numeric constraints |
2394
+ | [`BooleanBuilder`](lib/easy_talk/builders/boolean_builder.rb) | Boolean type (T::Boolean) |
2395
+ | [`TypedArrayBuilder`](lib/easy_talk/builders/typed_array_builder.rb) | Typed arrays (T::Array[Type]) |
2396
+ | [`CompositionBuilder`](lib/easy_talk/builders/composition_builder.rb) | Composition types (T::AnyOf, T::OneOf, T::AllOf) |
2397
+ | [`UnionBuilder`](lib/easy_talk/builders/union_builder.rb) | Nilable types (T.nilable) |
2398
+ | [`TemporalBuilder`](lib/easy_talk/builders/temporal_builder.rb) | Date and DateTime types |
2399
+ | [`BaseBuilder`](lib/easy_talk/builders/base_builder.rb) | Base class for all builders |
2400
+ | [`Registry`](lib/easy_talk/builders/registry.rb) | Type-to-builder registration |
2401
+
2402
+ ### Configuration & Utilities
2403
+
2404
+ | Class/Module | Description | Source |
2405
+ |--------------|-------------|--------|
2406
+ | [`EasyTalk::Configuration`](https://rubydoc.info/gems/easy_talk/EasyTalk/Configuration) | Global configuration options | [lib/easy_talk/configuration.rb](lib/easy_talk/configuration.rb) |
2407
+ | [`EasyTalk::ValidationBuilder`](https://rubydoc.info/gems/easy_talk/EasyTalk/ValidationBuilder) | Generates ActiveModel validations from constraints | [lib/easy_talk/validation_builder.rb](lib/easy_talk/validation_builder.rb) |
2408
+ | [`EasyTalk::ErrorFormatter`](https://rubydoc.info/gems/easy_talk/EasyTalk/ErrorFormatter) | Formats validation errors (flat, JSON Pointer, RFC 7807, JSON:API) | [lib/easy_talk/error_formatter.rb](lib/easy_talk/error_formatter.rb) |
2409
+ | [`EasyTalk::TypeIntrospection`](https://rubydoc.info/gems/easy_talk/EasyTalk/TypeIntrospection) | Type detection utilities | [lib/easy_talk/type_introspection.rb](lib/easy_talk/type_introspection.rb) |
2410
+ | [`EasyTalk::Tools::FunctionBuilder`](https://rubydoc.info/gems/easy_talk/EasyTalk/Tools/FunctionBuilder) | LLM function specification generator | [lib/easy_talk/tools/function_builder.rb](lib/easy_talk/tools/function_builder.rb) |
1183
2411
 
1184
2412
  ## License
1185
2413