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