json_model_rb 0.1.20 → 0.1.22

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +156 -572
  3. data/lib/json_model/builder/alias_builder.rb +10 -0
  4. data/lib/json_model/builder/array_builder.rb +24 -0
  5. data/lib/json_model/builder/base_builder.rb +24 -0
  6. data/lib/json_model/builder/composition/any_of_builder.rb +16 -0
  7. data/lib/json_model/builder/composition/builder.rb +47 -0
  8. data/lib/json_model/builder/composition/intersection_builder.rb +16 -0
  9. data/lib/json_model/builder/composition/one_of_builder.rb +16 -0
  10. data/lib/json_model/builder/composition/sum_builder.rb +16 -0
  11. data/lib/json_model/builder/composition.rb +6 -0
  12. data/lib/json_model/builder/constrained_builder.rb +83 -0
  13. data/lib/json_model/builder/default_builder.rb +14 -0
  14. data/lib/json_model/builder/enum_builder.rb +16 -0
  15. data/lib/json_model/builder/format_builder.rb +47 -0
  16. data/lib/json_model/builder/key_builder.rb +30 -0
  17. data/lib/json_model/builder/nested_builder.rb +31 -0
  18. data/lib/json_model/builder/primitive/boolean_builder.rb +16 -0
  19. data/lib/json_model/builder/primitive/builder.rb +25 -0
  20. data/lib/json_model/builder/primitive/integer_builder.rb +16 -0
  21. data/lib/json_model/builder/primitive/null_builder.rb +16 -0
  22. data/lib/json_model/builder/primitive/number_builder.rb +16 -0
  23. data/lib/json_model/builder/primitive/string_builder.rb +16 -0
  24. data/lib/json_model/builder/primitive.rb +7 -0
  25. data/lib/json_model/builder/ref_builder.rb +35 -0
  26. data/lib/json_model/builder/schema_builder.rb +14 -0
  27. data/lib/json_model/builder.rb +65 -0
  28. data/lib/json_model/config/options.rb +5 -2
  29. data/lib/json_model/config.rb +0 -2
  30. data/lib/json_model/errors.rb +0 -3
  31. data/lib/json_model/logic/predicates/methods.rb +17 -0
  32. data/lib/json_model/predicates.rb +3 -0
  33. data/lib/json_model/schema.rb +44 -78
  34. data/lib/json_model/schema_meta.rb +7 -28
  35. data/lib/json_model/types/alias.rb +33 -0
  36. data/lib/json_model/types/any_of.rb +51 -20
  37. data/lib/json_model/types/one_of.rb +51 -20
  38. data/lib/json_model/types/ref.rb +32 -0
  39. data/lib/json_model/types.rb +47 -9
  40. data/lib/json_model/version.rb +1 -1
  41. data/lib/json_model.rb +3 -7
  42. data/spec/config_spec.rb +0 -14
  43. data/spec/examples/file_system_spec.rb +71 -31
  44. data/spec/examples/user_spec.rb +69 -40
  45. data/spec/schema_meta_spec.rb +0 -40
  46. data/spec/schema_spec.rb +43 -59
  47. metadata +84 -58
  48. data/lib/json_model/composeable.rb +0 -35
  49. data/lib/json_model/errors/invalid_ref_mode_error.rb +0 -12
  50. data/lib/json_model/errors/type_error.rb +0 -8
  51. data/lib/json_model/errors/unknown_attribute_error.rb +0 -13
  52. data/lib/json_model/properties.rb +0 -86
  53. data/lib/json_model/property.rb +0 -54
  54. data/lib/json_model/ref_mode.rb +0 -9
  55. data/lib/json_model/type_spec/array.rb +0 -72
  56. data/lib/json_model/type_spec/castable.rb +0 -34
  57. data/lib/json_model/type_spec/composition/all_of.rb +0 -29
  58. data/lib/json_model/type_spec/composition/any_of.rb +0 -34
  59. data/lib/json_model/type_spec/composition/one_of.rb +0 -38
  60. data/lib/json_model/type_spec/composition.rb +0 -79
  61. data/lib/json_model/type_spec/const.rb +0 -35
  62. data/lib/json_model/type_spec/enum.rb +0 -35
  63. data/lib/json_model/type_spec/object.rb +0 -32
  64. data/lib/json_model/type_spec/primitive/boolean.rb +0 -13
  65. data/lib/json_model/type_spec/primitive/integer.rb +0 -21
  66. data/lib/json_model/type_spec/primitive/null.rb +0 -13
  67. data/lib/json_model/type_spec/primitive/number.rb +0 -14
  68. data/lib/json_model/type_spec/primitive/numeric.rb +0 -85
  69. data/lib/json_model/type_spec/primitive/string.rb +0 -150
  70. data/lib/json_model/type_spec/primitive.rb +0 -40
  71. data/lib/json_model/type_spec.rb +0 -82
  72. data/lib/json_model/types/all_of.rb +0 -29
  73. data/lib/json_model/types/array.rb +0 -29
  74. data/lib/json_model/types/boolean.rb +0 -7
  75. data/lib/json_model/types/const.rb +0 -23
  76. data/lib/json_model/types/enum.rb +0 -23
  77. data/lib/json_model/types/integer.rb +0 -23
  78. data/lib/json_model/types/null.rb +0 -7
  79. data/lib/json_model/types/number.rb +0 -23
  80. data/lib/json_model/types/string.rb +0 -23
  81. data/spec/properties_spec.rb +0 -76
  82. data/spec/property_spec.rb +0 -86
  83. data/spec/type_spec/array_spec.rb +0 -206
  84. data/spec/type_spec/castable_spec.rb +0 -19
  85. data/spec/type_spec/composition/all_of_spec.rb +0 -57
  86. data/spec/type_spec/composition/any_of_spec.rb +0 -54
  87. data/spec/type_spec/composition/one_of_spec.rb +0 -59
  88. data/spec/type_spec/composition_spec.rb +0 -90
  89. data/spec/type_spec/const_spec.rb +0 -18
  90. data/spec/type_spec/enum_spec.rb +0 -84
  91. data/spec/type_spec/primitive/boolean_spec.rb +0 -12
  92. data/spec/type_spec/primitive/integer_spec.rb +0 -57
  93. data/spec/type_spec/primitive/null_spec.rb +0 -12
  94. data/spec/type_spec/primitive/number_spec.rb +0 -12
  95. data/spec/type_spec/primitive/numeric_spec.rb +0 -176
  96. data/spec/type_spec/primitive/string_spec.rb +0 -119
  97. data/spec/type_spec_spec.rb +0 -32
data/README.md CHANGED
@@ -1,684 +1,268 @@
1
- # JSON Model
1
+ # JsonModel
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/json_model_rb.svg)](https://badge.fury.io/rb/json_model_rb)
4
4
  [![Ruby](https://github.com/gillesbergerp/json_model_rb/actions/workflows/ci.yml/badge.svg)](https://github.com/gillesbergerp/json_model_rb/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- A Ruby DSL for building JSON Schema definitions with a clean, declarative syntax. Define your schemas in Ruby and generate complete, standards-compliant JSON Schema documents.
7
+ `JsonModel` is a Ruby gem that extends `Dry::Struct` with JSON Schema generation capabilities. It allows you to define robust data models using `dry-types` and `dry-struct` and automatically generate their corresponding JSON Schema (Draft 7).
8
8
 
9
9
  ## Installation
10
10
 
11
11
  Add this line to your application's Gemfile:
12
12
 
13
13
  ```ruby
14
- gem 'json_model'
14
+ gem 'json_model_rb'
15
15
  ```
16
16
 
17
17
  And then execute:
18
18
 
19
- ```bash
20
- bundle install
21
- ```
22
-
23
- Or install it yourself as:
19
+ $ bundle install
24
20
 
25
- ```bash
26
- gem install json_model
27
- ```
21
+ ## Basic Usage
28
22
 
29
- ## Quick Start
23
+ To use `JsonModel`, include the `JsonModel::Schema` module in your `Dry::Struct` classes.
30
24
 
31
25
  ```ruby
32
26
  require 'json_model'
33
27
 
34
- class User
28
+ class User < Dry::Struct
35
29
  include JsonModel::Schema
36
30
 
37
- schema_id "https://example.com/schemas/user.json"
38
- title "User"
39
- description "A registered user in the system"
40
-
41
- property :name, type: String
42
- property :email, type: String, format: :email
43
- property :age, type: Integer, minimum: 0, maximum: 120, optional: true
31
+ attribute :name, JsonModel::Types::String
32
+ attribute :email, JsonModel::Types::Email
33
+ attribute? :age, JsonModel::Types::Integer.optional
44
34
  end
45
35
 
46
- # Generate the JSON Schema
47
- puts JSON.pretty_generate(User.as_schema)
36
+ # Generate JSON Schema
37
+ puts User.as_schema
38
+ # {
39
+ # :type=>"object",
40
+ # :properties=>{
41
+ # :name=>{:type=>"string"},
42
+ # :email=>{:type=>"string", :format=>"email"},
43
+ # :age=>{:anyOf=>[{:type=>"null"}, {:type=>"integer"}]}
44
+ # },
45
+ # :required=>[:email, :name]
46
+ # }
48
47
  ```
49
48
 
50
- **Output:**
51
- ```json
52
- {
53
- "$id": "https://example.com/schemas/user.json",
54
- "additionalProperties": false,
55
- "title": "User",
56
- "description": "A registered user in the system",
57
- "properties": {
58
- "age": {
59
- "type": "integer",
60
- "minimum": 0,
61
- "maximum": 120
62
- },
63
- "email": {
64
- "type": "string",
65
- "format": "email"
66
- },
67
- "name": {
68
- "type": "string"
69
- }
70
- },
71
- "required": [
72
- {
73
- "json_class": "Symbol",
74
- "s": "email"
75
- },
76
- {
77
- "json_class": "Symbol",
78
- "s": "name"
79
- }
80
- ],
81
- "type": "object"
82
- }
83
- ```
49
+ ## Types and Formats
84
50
 
85
- ## Schema Metadata
51
+ `JsonModel` provides a set of predefined types in `JsonModel::Types` that map directly to JSON Schema types and formats.
86
52
 
87
- You can set top-level schema metadata properties directly in your schema class:
53
+ ### Primitive Types
88
54
 
89
- ```ruby
90
- class Product
91
- include JsonModel::Schema
55
+ Most `Dry::Types` are automatically mapped to their JSON Schema equivalents:
92
56
 
93
- # Schema metadata
94
- schema_id "https://api.example.com/schemas/product.json"
95
- schema_version :draft_2020_12
96
- title "Product"
97
- description "A product available in the catalog"
98
-
99
- # Properties
100
- property :id, type: String
101
- property :name, type: String
102
- property :price, type: T::Float[minimum: 0]
103
- property :available, type: T::Boolean, default: true, optional: true
104
- end
105
- ```
57
+ | Dry::Type | JSON Schema Type |
58
+ | :--- | :--- |
59
+ | `JsonModel::Types::String` | `string` |
60
+ | `JsonModel::Types::Integer` | `integer` |
61
+ | `JsonModel::Types::Float` | `number` |
62
+ | `JsonModel::Types::Bool` | `boolean` |
63
+ | `JsonModel::Types::Nil` | `null` |
106
64
 
107
- ### Available Metadata Keywords
65
+ ### Format Types
108
66
 
109
- - **`schema_id`** - Sets the `$id` (unique URI identifier for the schema)
110
- - **`schema_version`** - Sets the `$schema` (JSON Schema version)
111
- - **`title`** - Human-readable title for the schema
112
- - **`description`** - Detailed explanation of the schema's purpose
113
- - **`additional_properties`** - Whether additional properties are allowed (default: `false`)
67
+ `JsonModel` includes specialized string types with `format` metadata:
114
68
 
115
- ## Data Types
69
+ - `JsonModel::Types::Email`: `format: 'email'`
70
+ - `JsonModel::Types::UUID`: `format: 'uuid'`
71
+ - `JsonModel::Types::URI`: `format: 'uri'`
72
+ - `JsonModel::Types::Date`: `format: 'date'`
73
+ - `JsonModel::Types::DateTime`: `format: 'date-time'`
74
+ - `JsonModel::Types::IPv4`: `format: 'ipv4'`
75
+ - `JsonModel::Types::IPv6`: `format: 'ipv6'`
76
+ - `JsonModel::Types::Hostname`: `format: 'hostname'`
116
77
 
117
- ### String Type
78
+ ### Collection Types
118
79
 
119
- ```ruby
120
- class StringExample
121
- include JsonModel::Schema
122
-
123
- # Basic string
124
- property :simple_string, type: String
125
-
126
- # String with length constraints
127
- property :username, type: T::String[min_length: 3, max_length: 20]
128
-
129
- # String with pattern (regex)
130
- property :product_code, type: T::String[pattern: /\A[A-Z]{3}-\d{4}\z/]
131
-
132
- # String with format
133
- property :email, type: T::String[format: :email]
134
- property :uri, type: T::String[format: :uri]
135
- property :hostname, type: T::String[format: :hostname]
136
- property :ipv4, type: T::String[format: :ipv4]
137
- property :ipv6, type: T::String[format: :ipv6]
138
- property :uuid, type: T::String[format: :uuid]
139
- property :date, type: T::String[format: :date]
140
- property :time, type: T::String[format: :time]
141
- property :datetime, type: T::String[format: :date_time]
142
- property :duration, type: T::String[format: :duration]
143
-
144
- # String with enum
145
- property :status, T::Enum["draft", "published", "archived"]
146
-
147
- # String with const
148
- property :api_version, T::Const["v1"]
149
-
150
- # Optional string
151
- property :nickname, type: String, optional: true
152
- end
80
+ - `JsonModel::Types::Array.of(Type)`: Mapped to `type: 'array'` with `items`.
81
+ - `JsonModel::Types::UniqueArray`: An array with `uniqueItems: true`.
153
82
 
154
- # Generate the JSON Schema
155
- puts JSON.pretty_generate(StringExample.as_schema)
156
- ```
157
-
158
- **Output:**
159
- ```json
160
- {
161
- "additionalProperties": false,
162
- "properties": {
163
- "date": {
164
- "type": "string",
165
- "format": "date"
166
- },
167
- "datetime": {
168
- "type": "string",
169
- "format": "date-time"
170
- },
171
- "duration": {
172
- "type": "string",
173
- "format": "duration"
174
- },
175
- "email": {
176
- "type": "string",
177
- "format": "email"
178
- },
179
- "hostname": {
180
- "type": "string",
181
- "format": "hostname"
182
- },
183
- "ipv4": {
184
- "type": "string",
185
- "format": "ipv4"
186
- },
187
- "ipv6": {
188
- "type": "string",
189
- "format": "ipv6"
190
- },
191
- "product_code": {
192
- "type": "string",
193
- "pattern": "\\A[A-Z]{3}-\\d{4}\\z"
194
- },
195
- "simple_string": {
196
- "type": "string"
197
- },
198
- "time": {
199
- "type": "string",
200
- "format": "time"
201
- },
202
- "uri": {
203
- "type": "string",
204
- "format": "uri"
205
- },
206
- "username": {
207
- "type": "string",
208
- "minLength": 3,
209
- "maxLength": 20
210
- },
211
- "uuid": {
212
- "type": "string",
213
- "format": "uuid"
214
- }
215
- },
216
- "required": [
217
- "date",
218
- "datetime",
219
- "duration",
220
- "email",
221
- "hostname",
222
- "ipv4",
223
- "ipv6",
224
- "product_code",
225
- "simple_string",
226
- "time",
227
- "uri",
228
- "username",
229
- "uuid"
230
- ],
231
- "type": "object"
232
- }
233
- ```
83
+ ### Constrained Types
234
84
 
235
- ### Number and Integer Types
85
+ `JsonModel` respects many `dry-types` constraints:
236
86
 
237
87
  ```ruby
238
- class NumericExample
239
- include JsonModel::Schema
88
+ attribute :age, JsonModel::Types::Integer.constrained(gteq: 18, lteq: 99)
89
+ # JSON Schema: { "type": "integer", "minimum": 18, "maximum": 99 }
240
90
 
241
- # Integer
242
- property :count, type: Integer
91
+ attribute :code, JsonModel::Types::String.constrained(format: /\A[A-Z]+\z/)
92
+ # JSON Schema: { "type": "string", "pattern": "^[A-Z]+$" }
93
+ ```
243
94
 
244
- # Integer with range
245
- property :port, type: T::Integer[minimum: 1024, maximum: 65535]
95
+ ## Advanced Types and Builders
246
96
 
247
- # Integer with exclusive bounds
248
- property :positive_int, type: T::Integer[exclusive_minimum: 0]
97
+ `JsonModel` shines when dealing with complex data structures like references and polymorphic types.
249
98
 
250
- # Number (float/double)
251
- property :price, type: T::Number[minimum: 0]
99
+ ### Local and External References
252
100
 
253
- # Number with multiple_of
254
- property :quantity, type: T::Integer[multiple_of: 10]
101
+ When a schema refers to another `JsonModel::Schema`, you can use `local` or `external` references to control how the `$ref` is generated.
255
102
 
256
- # Number with precision
257
- property :temperature, type: T::Number[minimum: -273.15, maximum: 1000.0]
258
-
259
- # Optional number
260
- property :discount, type: Float, optional: true
261
- end
103
+ #### Local References
262
104
 
263
- # Generate the JSON Schema
264
- puts JSON.pretty_generate(NumericExample.as_schema)
265
- ```
266
-
267
- **Output:**
268
- ```json
269
- {
270
- "additionalProperties": false,
271
- "properties": {
272
- "count": {
273
- "type": "integer"
274
- },
275
- "discount": {
276
- "type": "number"
277
- },
278
- "port": {
279
- "type": "integer",
280
- "minimum": 1024,
281
- "maximum": 65535
282
- },
283
- "positive_int": {
284
- "type": "integer",
285
- "exclusiveMinimum": 0
286
- },
287
- "price": {
288
- "type": "number",
289
- "minimum": 0
290
- },
291
- "quantity": {
292
- "type": "integer",
293
- "multipleOf": 10
294
- },
295
- "temperature": {
296
- "type": "number",
297
- "minimum": -273.15,
298
- "maximum": 1000.0
299
- }
300
- },
301
- "required": [
302
- "count",
303
- "port",
304
- "positive_int",
305
- "price",
306
- "quantity",
307
- "temperature"
308
- ],
309
- "type": "object"
310
- }
311
- ```
312
-
313
- ### Boolean Type
105
+ Use `.local` to generate a relative `$ref` to a definition within the same schema document. This will also add the referenced schema to the `$defs` (or `definitions`) section.
314
106
 
315
107
  ```ruby
316
- class BooleanExample
108
+ class Address < Dry::Struct
317
109
  include JsonModel::Schema
318
-
319
- property :is_active, type: T::Boolean
320
- property :has_agreed, type: T::Boolean, default: false
321
- property :enabled, type: T::Boolean, optional: true
110
+ attribute :city, JsonModel::Types::String
322
111
  end
323
112
 
324
- # Generate the JSON Schema
325
- puts JSON.pretty_generate(BooleanExample.as_schema)
113
+ class User < Dry::Struct
114
+ include JsonModel::Schema
115
+ # Generates "$ref": "#/$defs/Address"
116
+ attribute :address, Address.local
117
+ end
326
118
  ```
327
119
 
328
- **Output:**
329
- ```json
330
- {
331
- "additionalProperties": false,
332
- "properties": {
333
- "enabled": {
334
- "type": "boolean"
335
- },
336
- "has_agreed": {
337
- "type": "boolean",
338
- "default": false
339
- },
340
- "is_active": {
341
- "type": "boolean"
342
- }
343
- },
344
- "required": [
345
- "has_agreed",
346
- "is_active"
347
- ],
348
- "type": "object"
349
- }
350
- ```
120
+ #### External References
351
121
 
352
- ### Array Type
122
+ Use `.external` to generate an absolute `$ref` using the schema's `$id`. This is useful when you want to refer to a schema that is defined in another file or hosted at a specific URL.
353
123
 
354
124
  ```ruby
355
- class ArrayExample
125
+ class RemoteUser < Dry::Struct
356
126
  include JsonModel::Schema
357
-
358
- # Simple array
359
- property :tags, type: T::Array[String]
360
-
361
- # Array with constraints
362
- property :numbers, type: T::Array[Integer, min_items: 1, max_items: 10, unique_items: true]
127
+
128
+ schema_id "https://example.com/schemas/user.json"
129
+
130
+ attribute :name, JsonModel::Types::String
363
131
  end
364
132
 
365
- # Generate the JSON Schema
366
- puts JSON.pretty_generate(ArrayExample.as_schema)
367
- ```
368
-
369
- **Output:**
370
- ```json
371
- {
372
- "additionalProperties": false,
373
- "properties": {
374
- "numbers": {
375
- "type": "array",
376
- "items": {
377
- "type": "integer"
378
- },
379
- "minItems": 1,
380
- "maxItems": 10,
381
- "uniqueItems": true
382
- },
383
- "tags": {
384
- "type": "array",
385
- "items": {
386
- "type": "string"
387
- }
388
- }
389
- },
390
- "required": [
391
- "numbers",
392
- "tags"
393
- ],
394
- "type": "object"
395
- }
133
+ class Profile < Dry::Struct
134
+ include JsonModel::Schema
135
+ # Generates "$ref": "https://example.com/schemas/user.json"
136
+ attribute :user, RemoteUser.external
137
+ end
396
138
  ```
397
139
 
398
- ## Schema Composition
140
+ ### Composition and Polymorphism
399
141
 
400
- JSON Model supports powerful schema composition using `T::AllOf`, `T::AnyOf`, and `T::OneOf`:
142
+ `JsonModel` supports complex type compositions using standard `dry-types` operators and specialized polymorphic builders.
401
143
 
402
- ### AllOf - Must Match All Schemas
144
+ #### Sum Types (`anyOf`)
403
145
 
404
- Use `T::AllOf` when a value must validate against all provided schemas (intersection/combining schemas):
146
+ Simple sum types using the `|` operator are mapped to JSON Schema `anyOf`.
405
147
 
406
148
  ```ruby
407
- class PersonBase
408
- include JsonModel::Schema
409
-
410
- property :name, type: String
411
- property :age, type: T::Integer[minimum: 0], optional: true
412
- end
413
-
414
- class EmployeeDetails
415
- include JsonModel::Schema
416
-
417
- property :employee_id, type: T::String[pattern: /\AE-\d{4}\z/]
418
- property :department, type: String
419
- property :salary, type: T::Number[minimum: 0], optional: true
420
- end
421
-
422
- class Employee
423
- include JsonModel::Schema
149
+ attribute :id, JsonModel::Types::Integer | JsonModel::Types::String
150
+ # JSON Schema: { "anyOf": [{ "type": "integer" }, { "type": "string" }] }
151
+ ```
424
152
 
425
- title "Employee"
426
- description "Combines person and employee-specific properties"
153
+ #### Intersection Types (`allOf`)
427
154
 
428
- property :employee, type: T::AllOf[PersonBase, EmployeeDetails]
429
- end
155
+ Intersection types using the `&` operator are mapped to JSON Schema `allOf`. This is useful for combining multiple sets of constraints or schemas.
430
156
 
431
- # Generate the JSON Schema
432
- puts JSON.pretty_generate(Employee.as_schema)
433
- ```
157
+ ```ruby
158
+ Email = JsonModel::Types::String.constrained(format: /@/)
159
+ Unique = JsonModel::Types::String.constrained(min_size: 5)
434
160
 
435
- **Output:**
436
- ```json
437
- {
438
- "additionalProperties": false,
439
- "title": "Employee",
440
- "description": "Combines person and employee-specific properties",
441
- "properties": {
442
- "employee": {
443
- "allOf": [
444
- {
445
- "additionalProperties": false,
446
- "properties": {
447
- "age": {
448
- "type": "integer",
449
- "minimum": 0
450
- },
451
- "name": {
452
- "type": "string"
453
- }
454
- },
455
- "required": [
456
- "name"
457
- ],
458
- "type": "object"
459
- },
460
- {
461
- "additionalProperties": false,
462
- "properties": {
463
- "department": {
464
- "type": "string"
465
- },
466
- "employee_id": {
467
- "type": "string",
468
- "pattern": "\\AE-\\d{4}\\z"
469
- },
470
- "salary": {
471
- "type": "number",
472
- "minimum": 0
473
- }
474
- },
475
- "required": [
476
- "department",
477
- "employee_id"
478
- ],
479
- "type": "object"
480
- }
481
- ]
482
- }
483
- },
484
- "required": [
485
- "employee"
486
- ],
487
- "type": "object"
488
- }
161
+ attribute :contact, Email & Unique
162
+ # JSON Schema: { "allOf": [{ "type": "string", "pattern": "@" }, { "type": "string", "minLength": 5 }] }
489
163
  ```
490
164
 
491
- ### AnyOf - Must Match At Least One Schema
165
+ #### Polymorphic Types (`oneOf` / `anyOf`)
492
166
 
493
- Use `T::AnyOf` when a value must validate against one or more schemas (union/alternatives):
167
+ For more advanced polymorphic structures, especially tagged unions, `JsonModel` provides `one_of` and `any_of` builders. This is ideal for APIs that return different object types based on a "discriminator" field (e.g., `type` or `kind`).
494
168
 
495
169
  ```ruby
496
- class EmailContact
170
+ Circle = Class.new(Dry::Struct) do
497
171
  include JsonModel::Schema
498
-
499
- property :email, type: String, format: :email
172
+ attribute :radius, JsonModel::Types::Float
500
173
  end
501
174
 
502
- class PhoneContact
175
+ Square = Class.new(Dry::Struct) do
503
176
  include JsonModel::Schema
504
-
505
- property :phone, type: T::String[pattern: /\A\+?[1-9]\\d{1,14}\z/]
177
+ attribute :side, JsonModel::Types::Float
506
178
  end
507
179
 
508
- class AddressContact
509
- include JsonModel::Schema
510
-
511
- property :street, type: String
512
- property :city, type: String
180
+ Shape = JsonModel::Types.one_of(:type) do
181
+ on :circle, Circle
182
+ on :square, Square
513
183
  end
514
184
 
515
- class Contact
185
+ class Canvas < Dry::Struct
516
186
  include JsonModel::Schema
187
+ attribute :shapes, JsonModel::Types::Array.of(Shape)
188
+ end
189
+ ```
517
190
 
518
- title "Contact Method"
519
- description "Must provide at least one contact method"
191
+ ### Builders
520
192
 
521
- property :contact, type: T::AnyOf[EmailContact, PhoneContact, AddressContact]
522
- end
193
+ Internally, `JsonModel` uses a "Builder" pattern to translate `Dry::Types` into JSON Schema fragments. Every type registered in `JsonModel::Builder` has a corresponding builder class (e.g., `StringBuilder`, `ArrayBuilder`, `RefBuilder`).
523
194
 
524
- # Generate the JSON Schema
525
- puts JSON.pretty_generate(Contact.as_schema)
195
+ You can inspect how a specific type will be rendered:
196
+ ```ruby
197
+ builder = JsonModel::Builder.for(JsonModel::Types::Email)
198
+ builder.as_schema # => { type: 'string', format: 'email' }
526
199
  ```
527
200
 
528
- **Output:**
529
- ```json
530
- {
531
- "additionalProperties": false,
532
- "title": "Contact Method",
533
- "description": "Must provide at least one contact method",
534
- "properties": {
535
- "contact": {
536
- "anyOf": [
537
- {
538
- "additionalProperties": false,
539
- "properties": {
540
- "email": {
541
- "type": "string",
542
- "format": "email"
543
- }
544
- },
545
- "required": [
546
- "email"
547
- ],
548
- "type": "object"
549
- },
550
- {
551
- "additionalProperties": false,
552
- "properties": {
553
- "phone": {
554
- "type": "string",
555
- "pattern": "\\A\\+?[1-9]\\\\d{1,14}\\z"
556
- }
557
- },
558
- "required": [
559
- "phone"
560
- ],
561
- "type": "object"
562
- },
563
- {
564
- "additionalProperties": false,
565
- "properties": {
566
- "city": {
567
- "type": "string"
568
- },
569
- "street": {
570
- "type": "string"
571
- }
572
- },
573
- "required": [
574
- "city",
575
- "street"
576
- ],
577
- "type": "object"
578
- }
579
- ]
580
- }
581
- },
582
- "required": [
583
- "contact"
584
- ],
585
- "type": "object"
586
- }
587
- ```
201
+ ## JSON Schema Features Supported
588
202
 
589
- ### OneOf - Must Match Exactly One Schema
203
+ - `type` (string, number, integer, boolean, object, array, null)
204
+ - `properties` and `required`
205
+ - `enum` (via `Dry::Types::String.enum(...)`)
206
+ - `default` values
207
+ - `pattern` (via Regexp constraints)
208
+ - `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`
209
+ - `minLength`, `maxLength`
210
+ - `minItems`, `maxItems`, `uniqueItems`
211
+ - `anyOf`, `oneOf`, `allOf` (Sum and Intersection types)
212
+ - `$ref` and `$defs` for nested schemas
590
213
 
591
- Use `T::OneOf` when a value must validate against exactly one schema (exclusive alternatives):
214
+ ## Configuration
592
215
 
593
- ```ruby
594
- class CreditCardPayment
595
- include JsonModel::Schema
216
+ You can configure global options for `JsonModel`, such as naming strategies for properties and schema IDs.
596
217
 
597
- property :payment_type, T::Const["credit_card"]
598
- property :card_number, type: T::String[pattern: /\A\d{16}\z/]
599
- property :cvv, type: T::String[pattern: /\A\d{3,4}\z/]
600
- property :expiry, type: T::String[pattern: /\A\d{2}\/\d{2}\z/]
601
- end
218
+ ### Attribute Naming and Strategies
602
219
 
603
- class PayPalPayment
604
- include JsonModel::Schema
220
+ By default, JSON property names match the attribute names defined in your `Dry::Struct`. However, you can customize this globally or per attribute.
605
221
 
606
- property :payment_type, T::Const["paypal"]
607
- property :paypal_email, type: T::String[format: :email]
608
- end
222
+ #### Global Property Naming Strategy
609
223
 
610
- class BankTransferPayment
611
- include JsonModel::Schema
224
+ You can set a global strategy to automatically transform attribute names (which are usually snake_case in Ruby) to a different format in the JSON Schema (e.g., camelCase).
612
225
 
613
- property :payment_type, type: T::Const["bank_transfer"]
614
- property :iban, type: T::String[pattern: "^[A-Z]{2}\\d{2}[A-Z0-9]+$"]
615
- property :swift, type: String, optional: true
226
+ ```ruby
227
+ JsonModel.configure do |config|
228
+ # Available strategies: :identity (default), :camel_case, :pascal_case
229
+ config.property_naming_strategy = :camel_case
616
230
  end
231
+ ```
617
232
 
618
- class PaymentMethod
619
- include JsonModel::Schema
233
+ | Strategy | Ruby Attribute | JSON Property |
234
+ | :--- | :--- | :--- |
235
+ | `:identity` | `user_id` | `user_id` |
236
+ | `:camel_case` | `user_id` | `userId` |
237
+ | `:pascal_case` | `user_id` | `UserId` |
620
238
 
621
- title "Payment Method"
622
- description "Must specify exactly one payment method"
239
+ #### Explicit Aliasing
623
240
 
624
- property :payment, type: T::OneOf[CreditCardPayment, PayPalPayment, BankTransferPayment], discriminator: :payment_type
625
- end
241
+ You can override the global strategy for a specific attribute using the `.as(key)` method on the type.
626
242
 
627
- # Generate the JSON Schema
628
- puts JSON.pretty_generate(PaymentMethod.as_schema)
629
- ```
243
+ ```ruby
244
+ class User < Dry::Struct
245
+ include JsonModel::Schema
630
246
 
631
- **Output:**
632
- ```json
633
- {
634
- "additionalProperties": false,
635
- "title": "Payment Method",
636
- "description": "Must specify exactly one payment method",
637
- "properties": {
638
- "payment": {
639
- "oneOf": [
640
- {
641
- "additionalProperties": false,
642
- "type": "object"
643
- },
644
- {
645
- "additionalProperties": false,
646
- "type": "object"
647
- },
648
- {
649
- "additionalProperties": false,
650
- "type": "object"
651
- }
652
- ]
653
- }
654
- },
655
- "required": [
656
- "payment"
657
- ],
658
- "type": "object"
659
- }
247
+ # Forces the JSON property name to be 'ID' regardless of global strategy
248
+ attribute :id, JsonModel::Types::Integer.as(:ID)
249
+
250
+ # Also works via meta
251
+ attribute :email, JsonModel::Types::String.meta(as: :emailAddress)
252
+ end
660
253
  ```
661
254
 
662
- ## Use Cases
255
+ #### Schema ID Naming Strategy
663
256
 
664
- - **API Documentation**: Generate JSON schemas for API request/response validation
665
- - **Configuration Files**: Define and validate application configuration schemas
666
- - **Data Validation**: Validate incoming data against defined schemas
667
- - **Code Generation**: Use schemas to generate code in other languages
668
- - **OpenAPI/Swagger**: Generate OpenAPI schema definitions for your APIs
669
- - **Form Generation**: Generate forms from schema definitions
257
+ Similarly, you can configure how `$id` is automatically generated for schemas if not explicitly provided.
670
258
 
671
- ## Resources
672
-
673
- - [JSON Schema Specification](https://json-schema.org/)
674
- - [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/)
675
- - [JSON Schema Validator](https://www.jsonschemavalidator.net/)
676
- - [Draft 7 Reference](https://json-schema.org/draft-07/json-schema-release-notes.html)
259
+ ```ruby
260
+ JsonModel.configure do |config|
261
+ # Available strategies: :none (default), :class_name, :kebab_case_class_name, :snake_case_class_name
262
+ config.schema_id_naming_strategy = :kebab_case_class_name
263
+ config.schema_id_base_uri = "https://api.example.com/schemas/"
264
+ end
677
265
 
678
266
  ## License
679
267
 
680
268
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
681
-
682
- ## Credits
683
-
684
- Developed and maintained by [gillesbergerp](https://github.com/gillesbergerp).