Skip to content

Empty webhooks: {} injected when rendering OpenAPI 3.0 docs containing a same-named scalar value (regression in v0.35.1) #586

@asadtariq96

Description

@asadtariq96

Describe the bug

When rendering an OpenAPI 3.0.x document that does not declare a top-level webhooks key, libopenapi injects a spurious empty webhooks: {} into the rendered output if the literal string webhooks appears anywhere else in the document as a scalar value (for example as the value of a vendor extension like x-foo: { service: webhooks }, or a server URL ending in /webhooks).

webhooks is an OpenAPI 3.1+ top-level field. Emitting it for a 3.0.x document produces output that fails strict 3.0 schema validators (e.g. Spectral's oas3-schema: "Property webhooks is not expected to be here.").

This is a regression: v0.34.4 is fine, v0.35.1 through v0.37.3 are affected.

Minimal reproduction

package main

import (
	"fmt"
	"strings"

	"github.com/pb33f/libopenapi"
)

func main() {
	src := []byte(`openapi: 3.0.3
info:
  title: t
  version: "1.0.0"
x-foo:
  service: webhooks
paths:
  /a:
    get:
      responses:
        '200':
          description: OK
`)
	doc, _ := libopenapi.NewDocument(src)
	doc.BuildV3Model()
	out, _ := doc.Render()
	fmt.Println(string(out))
	fmt.Println("injected webhooks:", strings.Contains(string(out), "\nwebhooks:"))
}

Expected

The rendered document is unchanged — no webhooks key, because the source has no top-level webhooks key. (injected webhooks: false)

Actual (v0.35.1 – v0.37.3)

openapi: 3.0.3
info:
  title: t
  version: "1.0.0"
x-foo:
  service: webhooks
webhooks: {}        # <-- injected, invalid in OpenAPI 3.0.x
paths:
  /a:
    get:
      responses:
        '200':
          description: OK

(injected webhooks: true)

Root cause

Bisected to commit cda65e2 ("more model refactoring and cleanup", first released in v0.35.1).

In datamodel/low/v3/create_document.go, extractWebhooks calls low.ExtractMap[*PathItem](ctx, WebhooksLabel, root, idx) unconditionally and then guards on if hooksN != nil && hooksL != nil. When there is no genuine top-level webhooks key, ExtractMap resolves the label through the index and matches a same-named scalar value elsewhere in the document (e.g. the webhooks in service: webhooks). That returns a non-nil empty map plus non-nil key/value nodes, so doc.Webhooks is populated with an empty map, which the high-level model then renders as webhooks: {}.

Every sibling extractor (extractServers, extractTags, extractPaths, extractComponents) instead resolves the genuine top-level key via the pre-collected documentTopLevelNodes and guards on vn != nil, so they are not affected. extractWebhooks is the only one that bypasses that.

Suggested fix

Gate extractWebhooks on the genuine top-level node (nodes.webhooks), mirroring the sibling extractors. PR to follow.

Versions

  • libopenapi: v0.35.1v0.37.3 affected; v0.34.4 and earlier unaffected
  • Go: 1.25

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