json_model_rb 0.1.20 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +156 -572
- data/lib/json_model/builder/alias_builder.rb +10 -0
- data/lib/json_model/builder/array_builder.rb +24 -0
- data/lib/json_model/builder/base_builder.rb +24 -0
- data/lib/json_model/builder/composition/any_of_builder.rb +16 -0
- data/lib/json_model/builder/composition/builder.rb +47 -0
- data/lib/json_model/builder/composition/intersection_builder.rb +16 -0
- data/lib/json_model/builder/composition/one_of_builder.rb +16 -0
- data/lib/json_model/builder/composition/sum_builder.rb +16 -0
- data/lib/json_model/builder/composition.rb +6 -0
- data/lib/json_model/builder/constrained_builder.rb +83 -0
- data/lib/json_model/builder/default_builder.rb +14 -0
- data/lib/json_model/builder/enum_builder.rb +16 -0
- data/lib/json_model/builder/format_builder.rb +47 -0
- data/lib/json_model/builder/key_builder.rb +30 -0
- data/lib/json_model/builder/nested_builder.rb +31 -0
- data/lib/json_model/builder/primitive/boolean_builder.rb +16 -0
- data/lib/json_model/builder/primitive/builder.rb +25 -0
- data/lib/json_model/builder/primitive/integer_builder.rb +16 -0
- data/lib/json_model/builder/primitive/null_builder.rb +16 -0
- data/lib/json_model/builder/primitive/number_builder.rb +16 -0
- data/lib/json_model/builder/primitive/string_builder.rb +16 -0
- data/lib/json_model/builder/primitive.rb +7 -0
- data/lib/json_model/builder/ref_builder.rb +35 -0
- data/lib/json_model/builder/schema_builder.rb +14 -0
- data/lib/json_model/builder.rb +65 -0
- data/lib/json_model/config/options.rb +5 -2
- data/lib/json_model/config.rb +0 -2
- data/lib/json_model/errors.rb +0 -3
- data/lib/json_model/logic/predicates/methods.rb +17 -0
- data/lib/json_model/predicates.rb +3 -0
- data/lib/json_model/schema.rb +54 -78
- data/lib/json_model/schema_meta.rb +7 -28
- data/lib/json_model/types/alias.rb +33 -0
- data/lib/json_model/types/any_of.rb +51 -20
- data/lib/json_model/types/one_of.rb +51 -20
- data/lib/json_model/types/ref.rb +32 -0
- data/lib/json_model/types.rb +47 -9
- data/lib/json_model/version.rb +1 -1
- data/lib/json_model.rb +3 -7
- data/spec/config_spec.rb +0 -14
- data/spec/examples/file_system_spec.rb +71 -31
- data/spec/examples/user_spec.rb +69 -40
- data/spec/schema_meta_spec.rb +0 -40
- data/spec/schema_spec.rb +43 -59
- metadata +84 -58
- data/lib/json_model/composeable.rb +0 -35
- data/lib/json_model/errors/invalid_ref_mode_error.rb +0 -12
- data/lib/json_model/errors/type_error.rb +0 -8
- data/lib/json_model/errors/unknown_attribute_error.rb +0 -13
- data/lib/json_model/properties.rb +0 -86
- data/lib/json_model/property.rb +0 -54
- data/lib/json_model/ref_mode.rb +0 -9
- data/lib/json_model/type_spec/array.rb +0 -72
- data/lib/json_model/type_spec/castable.rb +0 -34
- data/lib/json_model/type_spec/composition/all_of.rb +0 -29
- data/lib/json_model/type_spec/composition/any_of.rb +0 -34
- data/lib/json_model/type_spec/composition/one_of.rb +0 -38
- data/lib/json_model/type_spec/composition.rb +0 -79
- data/lib/json_model/type_spec/const.rb +0 -35
- data/lib/json_model/type_spec/enum.rb +0 -35
- data/lib/json_model/type_spec/object.rb +0 -32
- data/lib/json_model/type_spec/primitive/boolean.rb +0 -13
- data/lib/json_model/type_spec/primitive/integer.rb +0 -21
- data/lib/json_model/type_spec/primitive/null.rb +0 -13
- data/lib/json_model/type_spec/primitive/number.rb +0 -14
- data/lib/json_model/type_spec/primitive/numeric.rb +0 -85
- data/lib/json_model/type_spec/primitive/string.rb +0 -150
- data/lib/json_model/type_spec/primitive.rb +0 -40
- data/lib/json_model/type_spec.rb +0 -82
- data/lib/json_model/types/all_of.rb +0 -29
- data/lib/json_model/types/array.rb +0 -29
- data/lib/json_model/types/boolean.rb +0 -7
- data/lib/json_model/types/const.rb +0 -23
- data/lib/json_model/types/enum.rb +0 -23
- data/lib/json_model/types/integer.rb +0 -23
- data/lib/json_model/types/null.rb +0 -7
- data/lib/json_model/types/number.rb +0 -23
- data/lib/json_model/types/string.rb +0 -23
- data/spec/properties_spec.rb +0 -76
- data/spec/property_spec.rb +0 -86
- data/spec/type_spec/array_spec.rb +0 -206
- data/spec/type_spec/castable_spec.rb +0 -19
- data/spec/type_spec/composition/all_of_spec.rb +0 -57
- data/spec/type_spec/composition/any_of_spec.rb +0 -54
- data/spec/type_spec/composition/one_of_spec.rb +0 -59
- data/spec/type_spec/composition_spec.rb +0 -90
- data/spec/type_spec/const_spec.rb +0 -18
- data/spec/type_spec/enum_spec.rb +0 -84
- data/spec/type_spec/primitive/boolean_spec.rb +0 -12
- data/spec/type_spec/primitive/integer_spec.rb +0 -57
- data/spec/type_spec/primitive/null_spec.rb +0 -12
- data/spec/type_spec/primitive/number_spec.rb +0 -12
- data/spec/type_spec/primitive/numeric_spec.rb +0 -176
- data/spec/type_spec/primitive/string_spec.rb +0 -119
- data/spec/type_spec_spec.rb +0 -32
data/README.md
CHANGED
|
@@ -1,684 +1,268 @@
|
|
|
1
|
-
#
|
|
1
|
+
# JsonModel
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/json_model_rb)
|
|
4
4
|
[](https://github.com/gillesbergerp/json_model_rb/actions/workflows/ci.yml)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
|
|
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 '
|
|
14
|
+
gem 'json_model_rb'
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
And then execute:
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
bundle install
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Or install it yourself as:
|
|
19
|
+
$ bundle install
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
gem install json_model
|
|
27
|
-
```
|
|
21
|
+
## Basic Usage
|
|
28
22
|
|
|
29
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
47
|
-
puts
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
`JsonModel` provides a set of predefined types in `JsonModel::Types` that map directly to JSON Schema types and formats.
|
|
86
52
|
|
|
87
|
-
|
|
53
|
+
### Primitive Types
|
|
88
54
|
|
|
89
|
-
|
|
90
|
-
class Product
|
|
91
|
-
include JsonModel::Schema
|
|
55
|
+
Most `Dry::Types` are automatically mapped to their JSON Schema equivalents:
|
|
92
56
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
###
|
|
65
|
+
### Format Types
|
|
108
66
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
78
|
+
### Collection Types
|
|
118
79
|
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
`JsonModel` respects many `dry-types` constraints:
|
|
236
86
|
|
|
237
87
|
```ruby
|
|
238
|
-
|
|
239
|
-
|
|
88
|
+
attribute :age, JsonModel::Types::Integer.constrained(gteq: 18, lteq: 99)
|
|
89
|
+
# JSON Schema: { "type": "integer", "minimum": 18, "maximum": 99 }
|
|
240
90
|
|
|
241
|
-
|
|
242
|
-
|
|
91
|
+
attribute :code, JsonModel::Types::String.constrained(format: /\A[A-Z]+\z/)
|
|
92
|
+
# JSON Schema: { "type": "string", "pattern": "^[A-Z]+$" }
|
|
93
|
+
```
|
|
243
94
|
|
|
244
|
-
|
|
245
|
-
property :port, type: T::Integer[minimum: 1024, maximum: 65535]
|
|
95
|
+
## Advanced Types and Builders
|
|
246
96
|
|
|
247
|
-
|
|
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
|
-
|
|
251
|
-
property :price, type: T::Number[minimum: 0]
|
|
99
|
+
### Local and External References
|
|
252
100
|
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
125
|
+
class RemoteUser < Dry::Struct
|
|
356
126
|
include JsonModel::Schema
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
140
|
+
### Composition and Polymorphism
|
|
399
141
|
|
|
400
|
-
|
|
142
|
+
`JsonModel` supports complex type compositions using standard `dry-types` operators and specialized polymorphic builders.
|
|
401
143
|
|
|
402
|
-
|
|
144
|
+
#### Sum Types (`anyOf`)
|
|
403
145
|
|
|
404
|
-
|
|
146
|
+
Simple sum types using the `|` operator are mapped to JSON Schema `anyOf`.
|
|
405
147
|
|
|
406
148
|
```ruby
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
426
|
-
description "Combines person and employee-specific properties"
|
|
153
|
+
#### Intersection Types (`allOf`)
|
|
427
154
|
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
157
|
+
```ruby
|
|
158
|
+
Email = JsonModel::Types::String.constrained(format: /@/)
|
|
159
|
+
Unique = JsonModel::Types::String.constrained(min_size: 5)
|
|
434
160
|
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
165
|
+
#### Polymorphic Types (`oneOf` / `anyOf`)
|
|
492
166
|
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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
|
|
185
|
+
class Canvas < Dry::Struct
|
|
516
186
|
include JsonModel::Schema
|
|
187
|
+
attribute :shapes, JsonModel::Types::Array.of(Shape)
|
|
188
|
+
end
|
|
189
|
+
```
|
|
517
190
|
|
|
518
|
-
|
|
519
|
-
description "Must provide at least one contact method"
|
|
191
|
+
### Builders
|
|
520
192
|
|
|
521
|
-
|
|
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
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
+
## Configuration
|
|
592
215
|
|
|
593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
607
|
-
property :paypal_email, type: T::String[format: :email]
|
|
608
|
-
end
|
|
222
|
+
#### Global Property Naming Strategy
|
|
609
223
|
|
|
610
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
619
|
-
|
|
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
|
-
|
|
622
|
-
description "Must specify exactly one payment method"
|
|
239
|
+
#### Explicit Aliasing
|
|
623
240
|
|
|
624
|
-
|
|
625
|
-
end
|
|
241
|
+
You can override the global strategy for a specific attribute using the `.as(key)` method on the type.
|
|
626
242
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
243
|
+
```ruby
|
|
244
|
+
class User < Dry::Struct
|
|
245
|
+
include JsonModel::Schema
|
|
630
246
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
255
|
+
#### Schema ID Naming Strategy
|
|
663
256
|
|
|
664
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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).
|