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.
data/README.md CHANGED
@@ -1,17 +1,854 @@
1
1
  # Schematist
2
2
 
3
- A Ruby DSL for building JSON Schema documents.
3
+ [![Gem Version](https://badge.fury.io/rb/schematist.svg)](https://rubygems.org/gems/schematist)
4
+ [![Gem Downloads](https://img.shields.io/gem/dt/schematist)](https://rubygems.org/gems/schematist)
5
+ [![codecov](https://codecov.io/gh/crmne/schematist/branch/main/graph/badge.svg)](https://codecov.io/gh/crmne/schematist)
6
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
4
7
 
5
- ## Status
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
- Schematist is the next home of [ruby_llm-schema](https://github.com/crmne/ruby_llm-schema). Today, installing `schematist` installs ruby_llm-schema and `require "schematist"` loads it, so you can point your Gemfile here now. The expanded library, a complete JSON Schema (draft 2020-12) toolkit under the `Schematist` name, lands in a future release.
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
- MIT License - see LICENSE file for details.
854
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).