Add support for Dependencies and Conditionals#31
Conversation
|
Hi @marcoroth and thanks for the PR! I have a few thoughts about the DSL: I'd prefer class PaymentSchema < RubyLLM::Schema
number :credit_card, required: false, requires: :billing_address
string :billing_address, required: false
endMaybe also number :credit_card, required: false, requires: %i[billing_address cvv]For class ShippingSchema < RubyLLM::Schema
boolean :domestic
string :state, required: false
string :country, required: false
given domestic: true do
requires :state
otherwise do
requires :country
end
end
end |
|
Thanks for the feedback @crmne, I updated the PR to use |
crmne
left a comment
There was a problem hiding this comment.
Thanks for the PR. The direction makes sense and the surface API fits the gem, but I think this needs one correctness fix before merging.
Required change
defined schemas do not carry the new conditional/dependency metadata into $defs.
The new merge path is wired into top-level output and inline/nested schema generation, but define still builds the $defs entry manually in lib/ruby_llm/schema/dsl/utilities.rb without calling the same merge helper. That means this sort of schema silently drops the rule:
class OrderSchema < RubyLLM::Schema
define :address do
string :country
string :state, required: false
given country: "US" do
requires :state
end
end
object :address, of: :address
endThe generated $defs[:address] contains the properties and required fields, but not the if/then rule. The same applies to requires: / dependent inside a define block. Since define is one of the existing reusable-schema paths, this creates inconsistent behavior between referenced schemas and inline schemas.
Please update define to include conditions/dependencies and add tests in the definitions/reference specs covering both given and dependency output inside a defined schema.
Style notes
The implementation mostly matches the existing codebase: the feature is isolated in a DSL module, uses the same keyword-option style, and keeps the schema hashes compact.
A few things I would adjust for consistency:
- Prefer testing more through
to_json_schemarather than directly assertingschema_class.conditions. Existing property specs mostly exercise the public output shape, which makes the tests less coupled to internal storage. merge_conditionslooks like an internal schema-output helper, but it is currently a public DSL method. Consider making it private or renaming/placing it so it is clearly not user-facing.- The README examples are useful, but the new section is much more exhaustive than nearby feature sections. I would tighten it a bit and keep the deeper behavior covered by specs.
I ran the suite locally and the current PR is green (bundle exec rspec, bundle exec rubocop --force-exclusion), so the blocker is about the missing $defs behavior rather than a failing existing test.
|
@crmne thanks for the review, just updated it 🙌🏼 |
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. Thanks for integrating Codecov - We've got you covered ☂️ |
crmne
left a comment
There was a problem hiding this comment.
Thanks for the updates. PR #31 is now linked to #44 and the overall direction matches the issue: given, dependent, and inline requires: are the right DSL shape for these JSON Schema keywords. The $defs propagation issue from the previous review is also fixed.
I still need a few changes before this is ready:
-
validatesdrops falsey constraint values.In
lib/ruby_llm/schema/dsl/conditionals.rb, the constraint builder uses truthiness checks:constraints[:const] = const if const constraints[:not] = {const: not_value} if not_value
This silently loses valid JSON Schema constraints such as
const: falseandnot_value: false, producing an empty property schema instead of the intended validation. Please preserve explicitly provided falsey values. Also consider howconst: nilshould be represented, sincenilis a valid JSON value but cannot be distinguished from the default with the current keyword signature. -
Inline
requires:only works on primitive properties.The issue describes inline property dependencies generally, but
requires:is only threaded throughPrimitiveTypes. Complex property methods still pass all options into their schema builders, so this raisesunknown keyword: :requires:object :payment, required: false, requires: :billing_address do string :method end
Please make inline
requires:consistent across property DSL methods (object,array,any_of,one_ofas well as primitives), or narrow the public API and docs/tests so the limitation is explicit. -
Please avoid changing the global RuboCop parameter-list limit for this feature.
.rubocop.ymlincreasesMetrics/ParameterListsfrom6to10to accommodateConditionalBuilder#validates. That weakens the project-wide style guard for one method. Refactorvalidatesinstead, for example by accepting**optionsand normalizing allowed keys, or by extracting a small constraint-building helper. -
Keep internal output helpers out of the public DSL surface.
merge_conditionsis an output-generation helper, andcoerce_conditionis also implementation detail. They are currently public class methods because they live alongside user DSL methods inConditionals. Please make these private or move the output merge behavior somewhere that is clearly internal. -
Reduce duplicated schema-building logic in
ConditionalBuilder#validates.The method manually maps DSL options to JSON Schema keys, separate from the existing schema builder path. That makes it easier for conditionals to drift from normal property behavior, and it is already where the falsey-value bug appears. Please align this with the existing builder style so condition branch validations behave predictably and stay easier to maintain.
-
Bring the README section closer to the surrounding documentation style.
The new examples are useful, but the section is more expansive than nearby property sections and uses
[!NOTE]callouts where the README mostly uses plain prose. Please tighten it to the public API and keep deeper edge behavior in specs.
I reran the current PR checks locally earlier and the suite/lint were green, so these are API correctness and maintainability changes rather than CI failures.
|
One clarification on the review: the falsey-value bug is a symptom of the same design issue as the duplicated validation builder.
Please treat that as the main fix: refactor the conditional validation path so it preserves explicitly supplied values and stays aligned with the existing schema builder behavior, rather than patching only the individual falsey checks. |
|
@crmne updated |
|
Thanks for the PR, it's now merged! The |
@crmne let me if you have a better idea on how to solve it |
|
A separate internal module/object would be cleaner, like |
Summary
Add
given,dependent, and inlinerequires:DSL methods for expressing JSON Schema conditionals and dependencies.These keywords are not typically supported by LLM providers for structured output, but
ruby_llm-schema's DSL is also used beyond LLM contexts, for example, RubyEvents uses it to define YAML validation schemas that are exported to JSON Schema and validated with yerba.requires:inline property dependenciesMaps to
dependentRequired. The simplest form for "if this property is present, require those":dependentproperty dependencies with validationsUse a block when you need
validates, upgrades output todependentSchemas:givenif/then/elseCondition values are auto-coerced: strings →
const, arrays →enum, regexps →pattern, hashes → raw schema.All three propagate through nested schemas via
of:anddefine/$defs.Closes #44