From fb6812df09a87d257d7e2ed6c6b5bd5515d849ff Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 14:04:04 +0000 Subject: [PATCH] feat(api): emit request bodies for non-GET operations Keep OpenAPI requestBody generation aligned with the RouteDescription contract by allowing non-GET operations, including DELETE, to declare JSON bodies. Co-Authored-By: Virgil --- openapi.go | 3 ++- openapi_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/openapi.go b/openapi.go index e88aa70..4d7624f 100644 --- a/openapi.go +++ b/openapi.go @@ -136,7 +136,8 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any { } // Add request body for methods that accept one. - if rd.RequestBody != nil && (method == "post" || method == "put" || method == "patch") { + // The contract only excludes GET; other verbs may legitimately carry bodies. + if rd.RequestBody != nil && method != "get" && method != "head" { operation["requestBody"] = map[string]any{ "required": true, "content": map[string]any{ diff --git a/openapi_test.go b/openapi_test.go index bd799f2..fe47995 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -328,6 +328,51 @@ func TestSpecBuilder_Good_OperationIDPreservesPathParams(t *testing.T) { } } +func TestSpecBuilder_Good_RequestBodyOnDelete(t *testing.T) { + sb := &api.SpecBuilder{ + Title: "Test", + Version: "1.0.0", + } + + group := &specStubGroup{ + name: "resources", + basePath: "/api", + descs: []api.RouteDescription{ + { + Method: "DELETE", + Path: "/resources/{id}", + Summary: "Delete resource", + Tags: []string{"resources"}, + RequestBody: map[string]any{ + "type": "object", + "properties": map[string]any{ + "reason": map[string]any{"type": "string"}, + }, + }, + Response: map[string]any{ + "type": "object", + }, + }, + }, + } + + data, err := sb.Build([]api.RouteGroup{group}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var spec map[string]any + if err := json.Unmarshal(data, &spec); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + paths := spec["paths"].(map[string]any) + deleteOp := paths["/api/resources/{id}"].(map[string]any)["delete"].(map[string]any) + if deleteOp["requestBody"] == nil { + t.Fatal("expected requestBody on DELETE /api/resources/{id}") + } +} + func TestSpecBuilder_Good_NonDescribableGroup(t *testing.T) { sb := &api.SpecBuilder{ Title: "Test",