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.1 … v0.37.3 affected; v0.34.4 and earlier unaffected
- Go: 1.25
Describe the bug
When rendering an OpenAPI 3.0.x document that does not declare a top-level
webhookskey, libopenapi injects a spurious emptywebhooks: {}into the rendered output if the literal stringwebhooksappears anywhere else in the document as a scalar value (for example as the value of a vendor extension likex-foo: { service: webhooks }, or a server URL ending in/webhooks).webhooksis an OpenAPI 3.1+ top-level field. Emitting it for a3.0.xdocument produces output that fails strict 3.0 schema validators (e.g. Spectral'soas3-schema: "Property webhooks is not expected to be here.").This is a regression:
v0.34.4is fine,v0.35.1throughv0.37.3are affected.Minimal reproduction
Expected
The rendered document is unchanged — no
webhookskey, because the source has no top-levelwebhookskey. (injected webhooks: false)Actual (v0.35.1 – v0.37.3)
(
injected webhooks: true)Root cause
Bisected to commit
cda65e2("more model refactoring and cleanup", first released inv0.35.1).In
datamodel/low/v3/create_document.go,extractWebhookscallslow.ExtractMap[*PathItem](ctx, WebhooksLabel, root, idx)unconditionally and then guards onif hooksN != nil && hooksL != nil. When there is no genuine top-levelwebhookskey,ExtractMapresolves the label through the index and matches a same-named scalar value elsewhere in the document (e.g. thewebhooksinservice: webhooks). That returns a non-nil empty map plus non-nil key/value nodes, sodoc.Webhooksis populated with an empty map, which the high-level model then renders aswebhooks: {}.Every sibling extractor (
extractServers,extractTags,extractPaths,extractComponents) instead resolves the genuine top-level key via the pre-collecteddocumentTopLevelNodesand guards onvn != nil, so they are not affected.extractWebhooksis the only one that bypasses that.Suggested fix
Gate
extractWebhookson the genuine top-level node (nodes.webhooks), mirroring the sibling extractors. PR to follow.Versions
v0.35.1…v0.37.3affected;v0.34.4and earlier unaffected