The 422 Generator
The 422 Generator: turn a ValidationError into the API error document your frontend actually wants - message, allowed values, bounds - using ONLY what the exception carries (new this round: #expectations). The renderer has zero knowledge of the contract; the exception brings the contract with it.
Data & Pipelines
Round 5
Piotr Solnica
exit 0
bundle exec ruby examples/form_errors.rb
a real captured run
submission #1: {:email=>"ada@example.com", :plan=>"team", :seats=>12}
201 CREATED ord-team-12
submission #2: {:email=>"", :plan=>"premium", :seats=>0}
{
"status": 422,
"capability": "checkout",
"errors": [
{
"field": "email",
"messages": [
"size cannot be less than 1"
],
"type": "string"
},
{
"field": "plan",
"messages": [
"must be one of: starter, team, enterprise"
],
"allowed": [
"starter",
"team",
"enterprise"
],
"type": "string"
},
{
"field": "seats",
"messages": [
"must be greater than or equal to 1"
],
"minimum": 1,
"maximum": 500,
"type": "number"
}
],
"policy_violations": []
}
submission #3: {:email=>"joan@example.com", :plan=>"starter", :seats=>9}
{
"status": 422,
"capability": "checkout",
"errors": [],
"policy_violations": [
{
"rule": "starter_seat_limit",
"message": "starter plan is limited to 5 seats",
"highlight_fields": [
"plan",
"seats"
]
}
]
}
the renderer never saw the checkout contract - 'allowed', 'minimum',
and 'maximum' all traveled inside the exception. one renderer serves
every capability in the app, current and future.
source
# frozen_string_literal: true # The 422 Generator: turn a ValidationError into the API error document # your frontend actually wants - message, allowed values, bounds - using # ONLY what the exception carries (new this round: #expectations). The # renderer has zero knowledge of the contract; the exception brings the # contract with it. # # bundle exec ruby examples/form_errors.rb # # Runs offline; prints the JSON your form would receive. require class="s">"bundler/setup" require class="s">"agentic" require class="s">"json" spec = Agentic:class="y">:CapabilitySpecification.new( name: class="s">"checkout", description: class="s">"Process a checkout form", version: class="s">"1.0.0", inputs: { email: {type: class="s">"string", required: true, non_empty: true}, plan: {type: class="s">"string", required: true, enum: %w[starter team enterprise]}, seats: {type: class="s">"number", required: true, min: 1, max: 500}, coupon: {type: class="s">"string"} }, rules: { starter_seat_limit: { message: class="s">"starter plan is limited to 5 seats", fields: [class="y">:plan, class="y">:seats], check: ->(i) { i[class="y">:plan] != class="s">"starter" || i[class="y">:seats] <= 5 } } } ) Agentic.register_capability(spec, Agentic:class="y">:CapabilityProvider.new( capability: spec, implementation: ->(i) { {order_id: class="s">"ord-#{i[class="y">:plan]}-#{i[class="y">:seats]}"} } )) # The renderer: exception in, error document out. Note what it does NOT # have: any reference to the checkout contract. def error_document(error) field_errors = error.violations.filter_map do |field, messages| next if field == class="y">:base declared = error.expectations[field] || {} detail = {field: field, messages: Array(messages)} detail[class="y">:allowed] = declared[class="y">:enum] if declared[class="y">:enum] detail[class="y">:minimum] = declared[class="y">:min] if declared[class="y">:min] detail[class="y">:maximum] = declared[class="y">:max] if declared[class="y">:max] detail[class="y">:type] = declared[class="y">:type] if declared[class="y">:type] detail end { status: 422, capability: error.capability, errors: field_errors, # Structured rule violations point at the widgets they involve policy_violations: error.rule_violations.map { |v| {rule: v[class="y">:rule], message: v[class="y">:message], highlight_fields: v[class="y">:fields]} } } end checkout = Agentic:class="y">:AgentCapabilityRegistry.instance.get_provider(class="s">"checkout") SUBMISSIONS = [ {email: class="s">"ada@example.com", plan: class="s">"team", seats: 12}, {email: class="s">"", plan: class="s">"premium", seats: 0}, {email: class="s">"joan@example.com", plan: class="s">"starter", seats: 9} ].freeze SUBMISSIONS.each_with_index do |form, index| puts class="s">"submission ##{index + 1}: #{form.inspect}" begin result = checkout.execute(form) puts class="s">" 201 CREATED #{result[class="y">:order_id]}" rescue Agentic:class="y">:Errors:class="y">:ValidationError => e puts JSON.pretty_generate(error_document(e)).gsub(/^/, class="s">" ") end puts end puts class="s">"the renderer never saw the checkout contract - 'allowed', 'minimum'," puts class="s">"and 'maximum' all traveled inside the exception. one renderer serves" puts class="s">"every capability in the app, current and future."