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.
data/README.md CHANGED
@@ -1,17 +1,767 @@
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 Ruby DSL for creating JSON schemas with a clean, Rails-inspired API.
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
+ 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
- MIT License - see LICENSE file for details.
767
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).