Skip to content

Numeric Range values lose enum precision in exported schema #66

Description

@numbata

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions