OpenAPI Schema Generation¶
Your task already declares its inputs — types, required fields, nested objects. That contract is the source of truth for validation.
Project it into OpenAPI and your HTTP docs stay aligned with what execute actually enforces. Change a required in the task, redeploy, and the schema updates with it.
Example task¶
Descriptions on each input become OpenAPI description fields.
class CreateUser < CMDx::Task
required :email, coerce: :string, description: "Primary email address",
validate: { format: URI::MailTo::EMAIL_REGEXP }
required :age, coerce: :integer, description: "Age in years",
validate: { numericality: { greater_than_or_equal_to: 13 } }
optional :newsletter, coerce: :boolean, default: false,
description: "Subscribe to the product newsletter"
required :address, description: "Shipping address" do
required :street, coerce: :string, description: "Street name and number"
required :city, coerce: :string, description: "City"
optional :zip_code, coerce: %i[string integer], description: "Postal code"
end
def work
context.user = User.create!(email:, age:, newsletter:, address:)
end
end
Setup — generator¶
inputs_schema returns one entry per input: { name:, description:, required:, options:, children: }. Walk it recursively to build a nested OpenAPI object.
Coerce arrays use the first symbol for the OpenAPI type — enough for most cases. Extend the map when you need format, enum, and so on.
class OpenApiSchemaGenerator
COERCE_TO_TYPE = {
string: "string",
symbol: "string",
date: "string",
date_time: "string",
time: "string",
integer: "integer",
float: "number",
big_decimal: "number",
boolean: "boolean",
array: "array",
hash: "object"
}.freeze
private_constant :COERCE_TO_TYPE
def self.generate(task_class)
new.build_object(task_class.inputs_schema.values)
end
def build_object(inputs)
properties = {}
required = []
inputs.each do |input|
properties[input[:name]] = build_property(input)
required << input[:name] if input[:required]
end
{ type: "object", properties:, required: }
end
def build_property(input)
prop = { description: input[:description] }.compact
coerce = Array(input.dig(:options, :coerce)).first
prop[:type] = COERCE_TO_TYPE.fetch(coerce, "string") if coerce
unless input[:children].empty?
nested = build_object(input[:children])
prop[:type] = "object"
prop[:properties] = nested[:properties]
prop[:required] = nested[:required] unless nested[:required].empty?
end
prop
end
end
Usage¶
What to expect¶
{
"type": "object",
"properties": {
"email": { "description": "Primary email address", "type": "string" },
"age": { "description": "Age in years", "type": "integer" },
"newsletter": { "description": "Subscribe to the product newsletter", "type": "boolean" },
"address": {
"description": "Shipping address",
"type": "object",
"properties": {
"street": { "description": "Street name and number", "type": "string" },
"city": { "description": "City", "type": "string" },
"zip_code": { "description": "Postal code", "type": "string" }
},
"required": ["street", "city"]
}
},
"required": ["email", "age", "address"]
}
Notes¶
Beyond the basics¶
:options carries the full declaration — :default, :validate, :if — so you can grow the generator toward default, enum (inclusion: { in: [...] }), pattern (format: { with: /.../ }), and minimum / maximum (numericality:). See Inputs — Definitions.