Skip to content

Bug: $ref to #/components/schemas/<N> fails with "does not exist" when N ≤ 99 #585

@balakumar28

Description

@balakumar28

Bug: $ref to #/components/schemas/<N> fails with "does not exist" when N ≤ 99

Repository: https://github.com/pb33f/libopenapi
Version: v0.31.1 (reproduced; likely affects earlier versions too)
Severity: Bug — valid OAS 3.x specifications are rejected


Summary

When an OpenAPI 3.x specification defines a component schema whose name is a small integer string
(e.g. "2", "10", "99"), BuildV3Model reports:

component `#/components/schemas/2` does not exist in the specification

even though the component is present in the document. The same error does not occur when
the schema name is a numeric string greater than 99 (e.g. "400", "422"), because those are
handled differently by an internal threshold check.


Minimal Reproducer

Save the following as bug.json and run BuildV3Model against it:

{
  "openapi": "3.0.3",
  "info": { "title": "Numeric Schema Key Bug", "version": "1.0.0" },
  "paths": {
    "/capture": {
      "post": {
        "operationId": "capturePayment",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/2" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Captured",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/2" }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/400" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "2": {
        "title": "Capture Request",
        "type": "object",
        "properties": {
          "amount": {
            "type": "object",
            "properties": {
              "currency_code": { "type": "string" },
              "value": { "type": "string" }
            }
          }
        }
      },
      "400": {
        "title": "Error 400",
        "type": "object",
        "properties": {
          "message": { "type": "string" }
        }
      }
    }
  }
}
data, _ := os.ReadFile("bug.json")
doc, _ := libopenapi.NewDocument(data)
_, err := doc.BuildV3Model()
fmt.Println(err)
// Output: component `#/components/schemas/2` does not exist in the specification

Expected: No error — both #/components/schemas/2 and #/components/schemas/400 resolve
successfully, because both keys exist under components.schemas.

Actual:

  • #/components/schemas/400 resolves correctly ✓
  • #/components/schemas/2 fails with "does not exist in the specification" ✗

Root Cause

The bug is in utils/utils.go, function ConvertComponentIdIntoFriendlyPathSearch.

When the function encounters a path segment that is a pure integer, it applies this logic
(abridged):

intVal, err := strconv.Atoi(segs[i])
if err == nil {
    if intVal <= 99 {
        // Treated as an ARRAY INDEX — appends [2] (no quotes)
        appendSegmentOptimized(segs, cleaned, i, false)  // wrapInQuotes = false
    } else {
        // Treated as a STRING KEY — appends ['400'] (with quotes)
        appendSegmentOptimized(segs, cleaned, i, true)   // wrapInQuotes = true
    }
    continue
}

What goes wrong for #/components/schemas/2

The input #/components/schemas/2 is split into segments ["#", "components", "schemas", "2"].
When segment "2" is processed:

Step Value
strconv.Atoi("2") intVal = 2, err = nil
intVal <= 99 true
wrapInQuotes false
Generated JSONPath suffix [2] (array index notation)
Final JSONPath $.components.schemas[2]

schemas[2] asks for the third element of an array named schemas. Since schemas is a
JSON object (map), not an array, the query returns nothing and the reference is reported as
missing.

Why #/components/schemas/400 works

"400" produces intVal = 400 > 99, so wrapInQuotes = true, resulting in
$.components.schemas['400'] — a correct property key lookup.

Affected range

Any component name that is a pure integer string with a value ≤ 99 triggers the bug.
Examples: "0", "1", "2", "10", "42", "99".

The threshold of 99 appears to have been intended to distinguish between array-style indices
(small integers used in allOf[0], oneOf[1], etc.) and large numeric schema names (HTTP
status codes). However, the two contexts cannot be distinguished purely by value magnitude —
components/schemas is always a map, never an array, regardless of whether the key is "2" or
"400".

This is a real-world issue encountered in the
PayPal Payments v2 API specification,
which defines schemas named "2", "400", "401", "403", "404", "409", and "422"
under #/components/schemas.


Suggested Fix

The fix should quote all numeric string keys that appear as the final segment of a
#/components/... path, without relying on a magnitude threshold.

One approach: after strconv.Atoi succeeds, check whether the parent path segment ends in
's' (as the existing plural-parent logic already does for non-numeric keys). If it does, the
numeric segment is a map key name, not an array index, and must be quoted:

intVal, err := strconv.Atoi(segs[i])
if err == nil {
    // If the parent segment ends in 's' (e.g. "schemas", "parameters"),
    // this integer is a map key name — always quote it.
    parentEndsSingular := i > 0 && len(segs[i-1]) > 0 && segs[i-1][len(segs[i-1])-1] == 's'
    if parentEndsSingular || intVal > 99 {
        if len(cleaned) > 0 {
            appendSegmentOptimized(segs, cleaned, i, true) // wrap in quotes
        }
    } else {
        if len(cleaned) > 0 {
            appendSegmentOptimized(segs, cleaned, i, false) // array index
        }
    }
    continue
}

Alternatively, always wrap numeric last-segments in quotes when the reference path starts with
#/components/ (since all components/* values are maps, never arrays).


Environment

Item Value
libopenapi version v0.31.1
Affected function utils.ConvertComponentIdIntoFriendlyPathSearch in utils/utils.go
Go version 1.21+
OAS version 3.0.x (also expected to affect 3.1.x)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions