schematist 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/LICENSE +6 -6
- data/README.md +754 -4
- data/lib/schematist/dsl/complex_types.rb +47 -0
- data/lib/schematist/dsl/conditional_builder.rb +72 -0
- data/lib/schematist/dsl/conditional_context.rb +24 -0
- data/lib/schematist/dsl/conditionals.rb +81 -0
- data/lib/schematist/dsl/primitive_types.rb +27 -0
- data/lib/schematist/dsl/schema_builders.rb +306 -0
- data/lib/schematist/dsl/utilities.rb +88 -0
- data/lib/schematist/dsl.rb +11 -0
- data/lib/schematist/errors.rb +28 -0
- data/lib/schematist/helpers.rb +10 -0
- data/lib/schematist/json_output.rb +73 -0
- data/lib/schematist/schema.rb +152 -0
- data/lib/schematist/validator.rb +91 -0
- data/lib/schematist/version.rb +1 -1
- data/lib/schematist.rb +44 -2
- data/lib/tasks/release.rake +8 -0
- metadata +26 -23
data/README.md
CHANGED
|
@@ -1,17 +1,767 @@
|
|
|
1
1
|
# Schematist
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/schematist)
|
|
4
|
+
[](https://rubygems.org/gems/schematist)
|
|
5
|
+
[](https://codecov.io/gh/crmne/schematist)
|
|
6
|
+
[](https://github.com/rubocop/rubocop)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
A Ruby DSL for creating JSON schemas with a clean, Rails-inspired API.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
Originally created by [Daniel Friis](https://github.com/danielfriis).
|
|
11
|
+
|
|
12
|
+
## Use Cases
|
|
13
|
+
|
|
14
|
+
JSON Schema is useful wherever Ruby code needs to describe structured data in a portable format.
|
|
15
|
+
|
|
16
|
+
Some ideal use cases:
|
|
17
|
+
|
|
18
|
+
- Defining API request and response shapes
|
|
19
|
+
- Describing configuration files or structured payloads
|
|
20
|
+
- Sharing validation contracts across systems
|
|
21
|
+
- Generating structured output schemas for LLM workflows
|
|
22
|
+
- Defining structured parameters for RubyLLM tools
|
|
23
|
+
|
|
24
|
+
### Simple Example
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class PersonSchema < Schematist::Schema
|
|
28
|
+
string :name, description: "Person's full name"
|
|
29
|
+
number :age, description: "Age in years", minimum: 0, maximum: 120
|
|
30
|
+
boolean :active, required: false
|
|
31
|
+
|
|
32
|
+
object :address do
|
|
33
|
+
string :street
|
|
34
|
+
string :city
|
|
35
|
+
string :country, required: false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
array :tags, of: :string, description: "User tags"
|
|
39
|
+
|
|
40
|
+
array :contacts do
|
|
41
|
+
object do
|
|
42
|
+
string :email, format: "email"
|
|
43
|
+
string :phone, required: false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
any_of :status do
|
|
48
|
+
string enum: ["active", "pending", "inactive"]
|
|
49
|
+
null
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Usage
|
|
54
|
+
schema = PersonSchema.new
|
|
55
|
+
puts schema.to_json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### RubyLLM structured output
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class PersonSchema < Schematist::Schema
|
|
62
|
+
string :name, description: "Person's full name"
|
|
63
|
+
integer :age, description: "Person's age in years"
|
|
64
|
+
string :city, required: false, description: "City where they live"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Use it natively with RubyLLM
|
|
68
|
+
chat = RubyLLM.chat
|
|
69
|
+
response = chat.with_schema(PersonSchema)
|
|
70
|
+
.ask("Generate a person named Alice who is 30 years old and lives in New York")
|
|
71
|
+
|
|
72
|
+
# The response is automatically parsed from JSON
|
|
73
|
+
puts response.content # => {"name" => "Alice", "age" => 30}
|
|
74
|
+
puts response.content.class # => Hash
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### RubyLLM tools
|
|
78
|
+
|
|
79
|
+
RubyLLM tools can use schema classes for structured parameters. This is useful when the same argument shape is shared across tools or elsewhere in your app.
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class SearchParams < Schematist::Schema
|
|
83
|
+
string :query, description: "Search query"
|
|
84
|
+
integer :limit, required: false, description: "Maximum results"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class SearchDocuments < RubyLLM::Tool
|
|
88
|
+
desc "Searches internal documents"
|
|
89
|
+
params SearchParams
|
|
90
|
+
|
|
91
|
+
def execute(query:, limit: 10)
|
|
92
|
+
DocumentSearch.call(query:, limit:)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For tool-specific parameters, define the schema inline with `params do ... end`.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
class Weather < RubyLLM::Tool
|
|
101
|
+
desc "Gets current weather"
|
|
102
|
+
|
|
103
|
+
params do
|
|
104
|
+
string :city, description: "City name"
|
|
105
|
+
string :units, enum: %w[celsius fahrenheit], required: false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def execute(city:, units: "celsius")
|
|
109
|
+
WeatherAPI.current(city:, units:)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
8
113
|
|
|
9
114
|
## Installation
|
|
10
115
|
|
|
116
|
+
Add this line to your application's Gemfile:
|
|
117
|
+
|
|
11
118
|
```ruby
|
|
12
119
|
gem 'schematist'
|
|
13
120
|
```
|
|
14
121
|
|
|
122
|
+
And then execute:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
bundle install
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or install it yourself as:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
gem install schematist
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Usage
|
|
135
|
+
|
|
136
|
+
Three approaches for creating schemas:
|
|
137
|
+
|
|
138
|
+
### Class Inheritance
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
class PersonSchema < Schematist::Schema
|
|
142
|
+
string :name, description: "Person's full name"
|
|
143
|
+
number :age
|
|
144
|
+
boolean :active, required: false
|
|
145
|
+
|
|
146
|
+
object :address do
|
|
147
|
+
string :street
|
|
148
|
+
string :city
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
array :tags, of: :string
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
schema = PersonSchema.new
|
|
155
|
+
puts schema.to_json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Factory Method
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
PersonSchema = Schematist::Schema.create do
|
|
162
|
+
string :name, description: "Person's full name"
|
|
163
|
+
number :age
|
|
164
|
+
boolean :active, required: false
|
|
165
|
+
|
|
166
|
+
object :address do
|
|
167
|
+
string :street
|
|
168
|
+
string :city
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
array :tags, of: :string
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
schema = PersonSchema.new
|
|
175
|
+
puts schema.to_json
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Global Helper
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
require 'schematist'
|
|
182
|
+
include Schematist::Helpers
|
|
183
|
+
|
|
184
|
+
person_schema = schema "PersonData", description: "A person object" do
|
|
185
|
+
string :name, description: "Person's full name"
|
|
186
|
+
number :age
|
|
187
|
+
boolean :active, required: false
|
|
188
|
+
|
|
189
|
+
object :address do
|
|
190
|
+
string :street
|
|
191
|
+
string :city
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
array :tags, of: :string
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
puts person_schema.to_json
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Schema Property Types
|
|
201
|
+
|
|
202
|
+
A schema is a collection of properties, which can be of different types. Each type has its own set of properties you can set.
|
|
203
|
+
|
|
204
|
+
All property types can (along with the required `name` key) be set with a `description` and a `required` flag (default is `true`).
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
string :name, description: "Person's full name"
|
|
208
|
+
number :age, description: "Person's age", required: false
|
|
209
|
+
boolean :is_active, description: "Whether the person is active"
|
|
210
|
+
null :placeholder, description: "A placeholder property"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Annotations
|
|
214
|
+
|
|
215
|
+
Annotations describe a schema for humans and tools. They carry no validation weight.
|
|
216
|
+
|
|
217
|
+
Supported annotations are `title`, `description`, `default`, `examples`, `deprecated`, `read_only`, and `write_only`.
|
|
218
|
+
|
|
219
|
+
Short annotations read well as keyword arguments:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
string :email,
|
|
223
|
+
title: "Email address",
|
|
224
|
+
description: "Primary contact email",
|
|
225
|
+
default: "user@example.com",
|
|
226
|
+
examples: ["alice@example.com"],
|
|
227
|
+
deprecated: false,
|
|
228
|
+
read_only: false,
|
|
229
|
+
write_only: false
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Longer ones read better inside the block, where they annotate the enclosing schema:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
object :account do
|
|
236
|
+
title "Account"
|
|
237
|
+
description "Billing account metadata used for invoices."
|
|
238
|
+
examples [{ id: "acct_123", status: "active" }]
|
|
239
|
+
|
|
240
|
+
string :id
|
|
241
|
+
string :status
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
They work at the root of a schema class and inside `define` too. When the same annotation is given both as a keyword and inside the block, the keyword wins.
|
|
246
|
+
|
|
247
|
+
⚠️ Please consult the LLM provider documentation for any limitations or restrictions. For example, as of now, OpenAI requires all properties to be required. In that case, you can use the `any_of` method to make a property optional.
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
any_of :name, description: "Person's full name" do
|
|
251
|
+
string
|
|
252
|
+
null
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Strings
|
|
257
|
+
|
|
258
|
+
String types support the following properties:
|
|
259
|
+
|
|
260
|
+
- `enum`: an array of allowed values (e.g. `enum: ["on", "off"]`)
|
|
261
|
+
- `const`: the single allowed value (e.g. `const: "admin"`)
|
|
262
|
+
- `pattern`: a regex pattern (e.g. `pattern: "\\d+"`)
|
|
263
|
+
- `format`: a format string (e.g. `format: "email"`)
|
|
264
|
+
- `min_length`: the minimum length of the string (e.g. `min_length: 3`)
|
|
265
|
+
- `max_length`: the maximum length of the string (e.g. `max_length: 10`)
|
|
266
|
+
|
|
267
|
+
Please consult the LLM provider documentation for the available formats and patterns.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
string :name, description: "Person's full name"
|
|
271
|
+
string :email, format: "email"
|
|
272
|
+
string :phone, pattern: "\\d+"
|
|
273
|
+
string :status, enum: ["on", "off"]
|
|
274
|
+
string :role, const: "admin"
|
|
275
|
+
string :code, min_length: 3, max_length: 10
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Encoded String Content
|
|
279
|
+
|
|
280
|
+
Strings that carry encoded content can describe what is inside them.
|
|
281
|
+
|
|
282
|
+
- `content_encoding`: how the string is encoded (e.g. `content_encoding: "base64"`)
|
|
283
|
+
- `content_media_type`: the media type of the decoded content (e.g. `content_media_type: "application/json"`)
|
|
284
|
+
- `content_schema`: a block describing the schema of the decoded content
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
string :payload, content_encoding: "base64", content_media_type: "application/json" do
|
|
288
|
+
content_schema do
|
|
289
|
+
object do
|
|
290
|
+
string :name
|
|
291
|
+
string :email
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Numbers
|
|
298
|
+
|
|
299
|
+
Number and integer types support the following properties:
|
|
300
|
+
|
|
301
|
+
- `enum`: an array of allowed numeric values (e.g. `enum: [0, 1, 2]`)
|
|
302
|
+
- `const`: the single allowed value (e.g. `const: 1`)
|
|
303
|
+
- `multiple_of`: a multiple of the number (e.g. `multiple_of: 0.01`)
|
|
304
|
+
- `minimum`: the minimum value of the number (e.g. `minimum: 0`)
|
|
305
|
+
- `maximum`: the maximum value of the number (e.g. `maximum: 100`)
|
|
306
|
+
- `greater_than`: an exclusive minimum (e.g. `greater_than: 0`)
|
|
307
|
+
- `less_than`: an exclusive maximum (e.g. `less_than: 100`)
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
number :price, minimum: 0, maximum: 100
|
|
311
|
+
number :score, greater_than: 0, less_than: 100
|
|
312
|
+
number :amount, multiple_of: 0.01
|
|
313
|
+
integer :level, enum: [0, 1, 2]
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Booleans
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
boolean :is_active
|
|
320
|
+
boolean :accepted_terms, const: true
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Boolean types only support `const`, the single allowed value.
|
|
324
|
+
|
|
325
|
+
### Null
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
null :placeholder
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Null types doesn't support any additional properties.
|
|
332
|
+
|
|
333
|
+
### Arrays
|
|
334
|
+
|
|
335
|
+
An array is a list of items. You can set the type of the items in the array with the `of` option or by passing a block with the `object` method.
|
|
336
|
+
|
|
337
|
+
An array can have a `min_items` and `max_items` option to set the minimum and maximum number of items in the array.
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
array :tags, of: :string # Array of strings
|
|
341
|
+
array :scores, of: :number # Array of numbers
|
|
342
|
+
array :items, min_items: 1, max_items: 10 # Array with size constraints
|
|
343
|
+
|
|
344
|
+
array :items do # Array of objects
|
|
345
|
+
object do
|
|
346
|
+
string :name
|
|
347
|
+
number :price
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
array :tags, of: :string, unique: true # No duplicate items
|
|
352
|
+
|
|
353
|
+
array :scores do # At least one score of 10 or more
|
|
354
|
+
integer
|
|
355
|
+
|
|
356
|
+
contains min: 1 do
|
|
357
|
+
integer minimum: 10
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Tuples
|
|
363
|
+
|
|
364
|
+
A tuple is a fixed-length array where each position has its own schema. It emits `prefixItems` along with matching `minItems` and `maxItems`.
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
tuple :coordinates do
|
|
368
|
+
number description: "Latitude"
|
|
369
|
+
number description: "Longitude"
|
|
370
|
+
end
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Objects
|
|
374
|
+
|
|
375
|
+
Objects types expect a block with the properties of the object.
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
object :user do
|
|
379
|
+
string :name
|
|
380
|
+
number :age
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
object :settings, description: "User preferences" do
|
|
384
|
+
boolean :notifications
|
|
385
|
+
string :theme, enum: ["light", "dark"]
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Object Key Constraints
|
|
390
|
+
|
|
391
|
+
Objects can constrain how many properties they carry, and what their keys look like.
|
|
392
|
+
|
|
393
|
+
- `min_properties` / `max_properties`: how many properties the object may have
|
|
394
|
+
- `keys`: a schema every property name must match, as JSON Schema `propertyNames`
|
|
395
|
+
- `keys_matching`: a schema for the properties whose names match a pattern, as JSON Schema `patternProperties`
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
object :metadata, min_properties: 1, max_properties: 10 do
|
|
399
|
+
keys do
|
|
400
|
+
string pattern: "^[a-z_]+$"
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
keys_matching(/^x-/) do
|
|
404
|
+
string
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
keys_matching(/^count_/) do
|
|
408
|
+
integer minimum: 0
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`keys` and `keys_matching` also work at the root of a schema class and inside `define`.
|
|
414
|
+
|
|
415
|
+
### Union Types (anyOf)
|
|
416
|
+
|
|
417
|
+
Union types are a way to specify that a property can be one of several types.
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
any_of :value do
|
|
421
|
+
string
|
|
422
|
+
number
|
|
423
|
+
null
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
any_of :identifier do
|
|
427
|
+
string description: "Username"
|
|
428
|
+
number description: "User ID"
|
|
429
|
+
end
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Composition (oneOf, allOf, not)
|
|
433
|
+
|
|
434
|
+
`one_of` matches exactly one of the given schemas, `all_of` matches all of them, and `none_of` matches none of them.
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
one_of :payment do
|
|
438
|
+
object do
|
|
439
|
+
string :card_number
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
object do
|
|
443
|
+
string :iban
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
all_of :account do
|
|
448
|
+
object do
|
|
449
|
+
string :id
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
object do
|
|
453
|
+
string :status
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
none_of :status do
|
|
458
|
+
string enum: ["deleted"]
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`none_of` with a single schema emits `not: { ... }`. With several, it emits `not: { anyOf: [...] }`.
|
|
463
|
+
|
|
464
|
+
### Unevaluated Properties and Items
|
|
465
|
+
|
|
466
|
+
`unevaluated_properties` and `unevaluated_items` constrain what is left over after composition, references, and conditionals have had their say. They are most useful on `all_of`, where `additional_properties` cannot see across the branches.
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
all_of :person, unevaluated_properties: false do
|
|
470
|
+
object do
|
|
471
|
+
string :name
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
object do
|
|
475
|
+
integer :age
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
object :profile, of: :person, unevaluated_properties: false
|
|
480
|
+
array :values, of: :integer, unevaluated_items: false
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Runtime Values
|
|
484
|
+
|
|
485
|
+
Any schema value can be a proc, resolved when the schema is rendered. That lets one schema class produce different documents per instance — useful when an enum comes from the database.
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
class RoleSchema < Schematist::Schema
|
|
489
|
+
description -> { "Roles available to #{@account.name}" }
|
|
490
|
+
|
|
491
|
+
string :role, enum: -> { @account.roles.pluck(:name) }
|
|
492
|
+
|
|
493
|
+
def initialize(account:)
|
|
494
|
+
super()
|
|
495
|
+
@account = account
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
RoleSchema.new(account: account).to_json_schema
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
A proc with no arguments is evaluated in the instance's context, so it can read instance variables. A proc that takes one argument receives the schema instance instead.
|
|
503
|
+
|
|
504
|
+
### Boolean and Raw Schemas
|
|
505
|
+
|
|
506
|
+
JSON Schema allows `true` and `false` in place of a schema object: `true` accepts every value, `false` accepts none. Inside a block, `any_schema` and `no_schema` emit them.
|
|
507
|
+
|
|
508
|
+
```ruby
|
|
509
|
+
any_of :value do
|
|
510
|
+
any_schema
|
|
511
|
+
string
|
|
512
|
+
end
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
When you need a keyword this DSL doesn't cover, `raw` emits a fragment verbatim.
|
|
516
|
+
|
|
517
|
+
```ruby
|
|
518
|
+
raw :role, { type: "string", const: "admin" }
|
|
519
|
+
|
|
520
|
+
any_of :value do
|
|
521
|
+
raw type: "string", const: "admin"
|
|
522
|
+
integer
|
|
523
|
+
end
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Schema Definitions and References
|
|
527
|
+
|
|
528
|
+
You can define sub-schemas and reference them in other schemas, or reference the root schema to generate recursive schemas.
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
class MySchema < Schematist::Schema
|
|
532
|
+
define :location do
|
|
533
|
+
string :latitude
|
|
534
|
+
string :longitude
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Using a reference in an array
|
|
538
|
+
array :coordinates, of: :location
|
|
539
|
+
|
|
540
|
+
# Using a reference in an object via the `reference` option
|
|
541
|
+
object :home_location, reference: :location
|
|
542
|
+
|
|
543
|
+
# Using a reference in an object via block
|
|
544
|
+
object :user do
|
|
545
|
+
reference :location
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Using a reference to the root schema
|
|
549
|
+
object :ui_schema do
|
|
550
|
+
string :element, enum: ["input", "button"]
|
|
551
|
+
string :label
|
|
552
|
+
object :sub_schema, reference: :root
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Core Keywords
|
|
558
|
+
|
|
559
|
+
Use core keywords when a schema or subschema needs an identifier, anchor, comment, dynamic reference, or vocabulary declaration.
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
class Node < Schematist::Schema
|
|
563
|
+
id "https://example.com/schemas/node"
|
|
564
|
+
comment "Internal note"
|
|
565
|
+
dynamic_anchor "node"
|
|
566
|
+
vocabulary "https://json-schema.org/draft/2020-12/vocab/core" => true
|
|
567
|
+
|
|
568
|
+
define :address do
|
|
569
|
+
anchor "address"
|
|
570
|
+
|
|
571
|
+
string :street
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
object :child do
|
|
575
|
+
dynamic_ref "#node"
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
`dynamic_ref` and `dynamic_anchor` are emitted verbatim. Their recursive resolution is the validator's job; this gem does not expand or interpret them.
|
|
581
|
+
|
|
582
|
+
### Nested Schemas
|
|
583
|
+
|
|
584
|
+
You can embed existing schema classes directly within objects or arrays for reusable schema composition.
|
|
585
|
+
|
|
586
|
+
```ruby
|
|
587
|
+
class PersonSchema < Schematist::Schema
|
|
588
|
+
string :name
|
|
589
|
+
integer :age
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
class CompanySchema < Schematist::Schema
|
|
593
|
+
# Using 'of' parameter
|
|
594
|
+
object :ceo, of: PersonSchema
|
|
595
|
+
array :employees, of: PersonSchema
|
|
596
|
+
|
|
597
|
+
# Using Schema.new in block
|
|
598
|
+
object :founder do
|
|
599
|
+
PersonSchema.new
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
schema = CompanySchema.new
|
|
604
|
+
schema.to_json_schema
|
|
605
|
+
# =>
|
|
606
|
+
# {
|
|
607
|
+
# "$schema":"https://json-schema.org/draft/2020-12/schema",
|
|
608
|
+
# "title":"CompanySchema",
|
|
609
|
+
# "type":"object",
|
|
610
|
+
# "properties":{
|
|
611
|
+
# "ceo":{
|
|
612
|
+
# "type":"object",
|
|
613
|
+
# "properties":{
|
|
614
|
+
# "name":{"type":"string"},
|
|
615
|
+
# "age":{"type":"integer"}
|
|
616
|
+
# },
|
|
617
|
+
# "required":["name","age"],
|
|
618
|
+
# "additionalProperties":false
|
|
619
|
+
# },
|
|
620
|
+
# "employees":{
|
|
621
|
+
# "type":"array",
|
|
622
|
+
# "items":{
|
|
623
|
+
# "type":"object",
|
|
624
|
+
# "properties":{
|
|
625
|
+
# "name":{"type":"string"},
|
|
626
|
+
# "age":{"type":"integer"}
|
|
627
|
+
# },
|
|
628
|
+
# "required":["name","age"],
|
|
629
|
+
# "additionalProperties":false
|
|
630
|
+
# }
|
|
631
|
+
# },
|
|
632
|
+
# "founder":{
|
|
633
|
+
# "type":"object",
|
|
634
|
+
# "properties":{
|
|
635
|
+
# "name":{"type":"string"},
|
|
636
|
+
# "age":{"type":"integer"}
|
|
637
|
+
# },
|
|
638
|
+
# "required":["name","age"],
|
|
639
|
+
# "additionalProperties":false
|
|
640
|
+
# }
|
|
641
|
+
# },
|
|
642
|
+
# "required":["ceo","employees","founder"],
|
|
643
|
+
# "additionalProperties":false
|
|
644
|
+
# }
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Dependencies
|
|
648
|
+
|
|
649
|
+
Use `requires:` inline or `dependent` block to express that the presence of one property requires others. Maps to [`dependentRequired`](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentRequired) (Draft 2019-09) and [`dependentSchemas`](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentSchemas) (Draft 2019-09). Check your provider's documentation for compatibility.
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
class PaymentSchema < Schematist::Schema
|
|
653
|
+
string :name
|
|
654
|
+
number :credit_card, required: false, requires: %i[billing_address cvv]
|
|
655
|
+
string :billing_address, required: false
|
|
656
|
+
string :cvv, required: false
|
|
657
|
+
end
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Use a `dependent` block when you also need validations — this upgrades the output to `dependentSchemas`:
|
|
661
|
+
|
|
662
|
+
```ruby
|
|
663
|
+
dependent :credit_card do
|
|
664
|
+
requires :billing_address
|
|
665
|
+
validates :billing_address, type: :string, min_length: 1
|
|
666
|
+
end
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Conditionals
|
|
670
|
+
|
|
671
|
+
Use `given` to add [JSON Schema `if`/`then`/`else`](https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse) (Draft 7) rules. Condition values are automatically coerced: strings → `const`, arrays → `enum`, regexps → `pattern`, hashes → raw schema.
|
|
672
|
+
|
|
673
|
+
```ruby
|
|
674
|
+
class OrderSchema < Schematist::Schema
|
|
675
|
+
string :status, enum: ["pending", "shipped", "cancelled"]
|
|
676
|
+
string :tracking_number, required: false
|
|
677
|
+
string :cancellation_reason, required: false
|
|
678
|
+
|
|
679
|
+
given status: "shipped" do
|
|
680
|
+
requires :tracking_number
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
given status: "cancelled" do
|
|
684
|
+
requires :cancellation_reason
|
|
685
|
+
validates :cancellation_reason, type: :string, min_length: 1
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
`validates` supports: `type:`, `not_value:`, `min_length:`, `max_length:`, `pattern:` (string or regexp), `enum:`, `const:`, `minimum:`, `maximum:`.
|
|
691
|
+
|
|
692
|
+
Use `otherwise` for an `else` branch:
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
given domestic: true do
|
|
696
|
+
requires :state
|
|
697
|
+
|
|
698
|
+
otherwise do
|
|
699
|
+
requires :country
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Conditions propagate through nested schemas via `of:`.
|
|
705
|
+
|
|
706
|
+
## JSON Output
|
|
707
|
+
|
|
708
|
+
`to_json_schema` returns a Draft 2020-12 JSON Schema document with string keys, ready to hand to any JSON Schema validator.
|
|
709
|
+
|
|
710
|
+
```ruby
|
|
711
|
+
schema = PersonSchema.new
|
|
712
|
+
schema.to_json_schema
|
|
713
|
+
# => {
|
|
714
|
+
# "$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
715
|
+
# "title" => "PersonSchema",
|
|
716
|
+
# "type" => "object",
|
|
717
|
+
# "properties" => { ... },
|
|
718
|
+
# "required" => [...],
|
|
719
|
+
# "additionalProperties" => false
|
|
720
|
+
# }
|
|
721
|
+
|
|
722
|
+
puts schema.to_json # Pretty JSON string of the same document
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
The schema name maps to `title`. Provider-only keys are not part of the document — `strict` was an OpenAI `response_format` flag, not a JSON Schema keyword, and has been removed. Set it where you build the request.
|
|
726
|
+
|
|
727
|
+
### Migrating from ruby_llm-schema
|
|
728
|
+
|
|
729
|
+
Schematist was called `ruby_llm-schema`. The old name put a general-purpose JSON Schema DSL inside another gem's namespace and implied it only made sense alongside an LLM client, which was never true.
|
|
730
|
+
|
|
731
|
+
Update the gem, then the constants:
|
|
732
|
+
|
|
733
|
+
```ruby
|
|
734
|
+
gem 'schematist' # was: gem 'ruby_llm-schema'
|
|
735
|
+
|
|
736
|
+
class PersonSchema < Schematist::Schema # was: RubyLLM::Schema
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
include Schematist::Helpers # was: RubyLLM::Helpers
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Errors moved up a level with the rename — `Schematist::ValidationError`, not `RubyLLM::Schema::ValidationError`. `strict` is gone; see below.
|
|
743
|
+
|
|
744
|
+
### Migrating from the provider envelope
|
|
745
|
+
|
|
746
|
+
`to_json_schema` used to return a provider envelope — `{name:, description:, schema:, strict:}`, the shape OpenAI's `response_format` expects. That envelope is gone. Building it is the provider client's job, not this gem's.
|
|
747
|
+
|
|
748
|
+
If you were reaching into `[:schema]` to get at the document, drop the digging — `to_json_schema` now returns the document itself. Note its keys are strings, not symbols:
|
|
749
|
+
|
|
750
|
+
```ruby
|
|
751
|
+
schema.to_json_schema[:schema][:properties] # before
|
|
752
|
+
schema.to_json_schema["properties"] # now
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
If you need the envelope for a provider that expects it, build it where you send it:
|
|
756
|
+
|
|
757
|
+
```ruby
|
|
758
|
+
{
|
|
759
|
+
name: "PersonSchema",
|
|
760
|
+
schema: PersonSchema.new.to_json_schema,
|
|
761
|
+
strict: true
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
15
765
|
## License
|
|
16
766
|
|
|
17
|
-
|
|
767
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|