Problem
When an API defines a property with a small set of allowed integer values using a Range (e.g., values: -2..2), the generated OpenAPI schema only contains minimum/maximum constraints. It does not include an enum array with the discrete allowed values.
This causes problems for downstream consumers — particularly code generators — that rely on enum to produce precise types. For example, a TypeScript generator seeing enum: [-2, -1, 0, 1, 2] can emit the literal union type -2 | -1 | 0 | 1 | 2, which provides compile-time safety. Without enum, it can only emit number, losing all type precision.
From a product perspective, this affects any team that generates typed API clients from the OpenAPI spec. A client expecting difficulty: -2 | -1 | 0 | 1 | 2 gets difficulty: number instead, which means invalid values like 99 are silently accepted at compile time and only caught at runtime (or worse, not at all).
Who is affected
- Teams generating typed SDKs or clients (TypeScript, Swift, Kotlin, etc.)
- API consumers who rely on the OpenAPI spec as documentation for allowed values
- Tooling like Swagger UI that renders enum values as dropdowns
Current behavior
Given a Grape entity exposure:
class SettingsEntity < Grape::Entity
expose :difficulty, documentation: {
type: Integer,
desc: 'Difficulty level',
values: -2..2
}
end
The OAS2 output is:
{
"difficulty": {
"type": "integer",
"format": "int32",
"minimum": -2,
"maximum": 2
}
}
Expected behavior
For small bounded integer ranges, the output should also include an enum array:
{
"difficulty": {
"type": "integer",
"format": "int32",
"minimum": -2,
"maximum": 2,
"enum": [-2, -1, 0, 1, 2]
}
}
This preserves backward compatibility (min/max constraints stay) while restoring precision for consumers that understand enum.
Root cause
`RangeUtils.apply_to_schema` (in `lib/grape_oas/range_utils.rb`) handles numeric and non-numeric ranges differently:
- Non-numeric ranges (e.g., `"a".."e"`) → expanded to `enum` via `expand_range_to_enum`
- Numeric ranges (e.g., `-2..2`) → only `minimum`/`maximum` via `apply_numeric_range`
There is no code path that produces `enum` from a numeric Range. The same pattern applies in both request parameters (`SchemaEnhancer.apply_values`) and response entity exposures (`ExposureProcessor.apply_exposure_properties`).
Possible solution
In `RangeUtils.apply_to_schema`, after applying numeric range constraints, also expand the range to an enum array when the range is small enough:
elsif numeric_range && numeric_type
apply_numeric_range(schema, range)
# Also emit enum for small bounded integer ranges
if schema.type == Constants::SchemaTypes::INTEGER && bounded?(range)
values = range.to_a
schema.enum = values if values.size <= max_enum_range_size
end
This keeps `minimum`/`maximum` for validators and generic consumers while adding `enum` for code generators and documentation tools.
The threshold for expanding a range into enum values should be configurable — different consumers have different needs. A project generating typed SDKs may want `enum` for ranges up to 20 values, while another may prefer to keep the schema lean and only expand ranges up to 5. A sensible default (e.g., the existing `MAX_ENUM_RANGE_SIZE = 100`) avoids breaking current behavior, but users should be able to tune it via configuration:
GrapeOAS.configure do |config|
config.max_enum_range_size = 20
end
Setting it to `0` would disable numeric range enum expansion entirely, preserving the current behavior for users who don't need it.
Note: this should only apply to integer ranges, not floats/doubles, since continuous numeric ranges cannot be meaningfully enumerated.
Problem
When an API defines a property with a small set of allowed integer values using a Range (e.g.,
values: -2..2), the generated OpenAPI schema only containsminimum/maximumconstraints. It does not include anenumarray with the discrete allowed values.This causes problems for downstream consumers — particularly code generators — that rely on
enumto produce precise types. For example, a TypeScript generator seeingenum: [-2, -1, 0, 1, 2]can emit the literal union type-2 | -1 | 0 | 1 | 2, which provides compile-time safety. Without enum, it can only emitnumber, losing all type precision.From a product perspective, this affects any team that generates typed API clients from the OpenAPI spec. A client expecting
difficulty: -2 | -1 | 0 | 1 | 2getsdifficulty: numberinstead, which means invalid values like99are silently accepted at compile time and only caught at runtime (or worse, not at all).Who is affected
Current behavior
Given a Grape entity exposure:
The OAS2 output is:
{ "difficulty": { "type": "integer", "format": "int32", "minimum": -2, "maximum": 2 } }Expected behavior
For small bounded integer ranges, the output should also include an
enumarray:{ "difficulty": { "type": "integer", "format": "int32", "minimum": -2, "maximum": 2, "enum": [-2, -1, 0, 1, 2] } }This preserves backward compatibility (min/max constraints stay) while restoring precision for consumers that understand
enum.Root cause
`RangeUtils.apply_to_schema` (in `lib/grape_oas/range_utils.rb`) handles numeric and non-numeric ranges differently:
There is no code path that produces `enum` from a numeric Range. The same pattern applies in both request parameters (`SchemaEnhancer.apply_values`) and response entity exposures (`ExposureProcessor.apply_exposure_properties`).
Possible solution
In `RangeUtils.apply_to_schema`, after applying numeric range constraints, also expand the range to an enum array when the range is small enough:
This keeps `minimum`/`maximum` for validators and generic consumers while adding `enum` for code generators and documentation tools.
The threshold for expanding a range into enum values should be configurable — different consumers have different needs. A project generating typed SDKs may want `enum` for ranges up to 20 values, while another may prefer to keep the schema lean and only expand ranges up to 5. A sensible default (e.g., the existing `MAX_ENUM_RANGE_SIZE = 100`) avoids breaking current behavior, but users should be able to tune it via configuration:
Setting it to `0` would disable numeric range enum expansion entirely, preserving the current behavior for users who don't need it.
Note: this should only apply to integer ranges, not floats/doubles, since continuous numeric ranges cannot be meaningfully enumerated.