From a40bc6de132f35a6a5d3f88b71f9c2fb2306393d Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Wed, 1 Jul 2026 01:53:38 +0800 Subject: [PATCH] fix(go-server): skip readOnly fields in AssertXxxRequired AssertXxxRequired is only invoked on decoded request bodies, but it enforced every entry of a schema's required list, including readOnly properties. Per OpenAPI 3.x, readOnly properties are produced by the server and MUST NOT be sent by the client, so a schema legitimately listing a readOnly property as required (e.g. Redfish/JSON:API identity fields like id, @odata.type) caused the generated controller to reject every valid request that omitted the readOnly field. Skip readOnly entries in the requiredVars loop in model.mustache so non-readOnly required fields are still validated while readOnly ones are not enforced on request bodies. Add a regression test using a spec with a readOnly+required id and a non-readOnly required name. --- .../main/resources/go-server/model.mustache | 8 +++- .../codegen/goserver/GoServerCodegenTest.java | 31 +++++++++++++ .../3_0/go-server/readonly-required.yaml | 45 +++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/go-server/readonly-required.yaml diff --git a/modules/openapi-generator/src/main/resources/go-server/model.mustache b/modules/openapi-generator/src/main/resources/go-server/model.mustache index e94a8b9a2dd8..28dd5d3cb61e 100644 --- a/modules/openapi-generator/src/main/resources/go-server/model.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/model.mustache @@ -80,8 +80,8 @@ type {{classname}} struct { func Assert{{classname}}Required(obj {{classname}}) error { {{#hasRequired}} elements := map[string]interface{}{ -{{#requiredVars}} "{{baseName}}": obj.{{name}}, -{{/requiredVars}} } +{{#requiredVars}}{{^isReadOnly}} "{{baseName}}": obj.{{name}}, +{{/isReadOnly}}{{/requiredVars}} } for name, el := range elements { if isZero := IsZeroValue(el); isZero { return &RequiredError{Field: name} @@ -100,6 +100,7 @@ func Assert{{classname}}Required(obj {{classname}}) error { {{/isMap}} {{/parent}} {{#Vars}} +{{^isReadOnly}} {{#isNullable}} {{#isModel}} if obj.{{name}} != nil { @@ -157,6 +158,7 @@ func Assert{{classname}}Required(obj {{classname}}) error { {{/items.isModel}} {{/isArray}} {{/isNullable}} +{{/isReadOnly}} {{/Vars}} return nil } @@ -164,6 +166,7 @@ func Assert{{classname}}Required(obj {{classname}}) error { // Assert{{classname}}Constraints checks if the values respects the defined constraints func Assert{{classname}}Constraints(obj {{classname}}) error { {{#Vars}} +{{^isReadOnly}} {{#minimum}} if {{#isNullable}}obj.{{name}} != nil && *{{/isNullable}}obj.{{name}} < {{minimum}} { return &ParsingError{Param: "{{name}}", Err: errors.New(errMsgMinValueConstraint)} @@ -229,6 +232,7 @@ func Assert{{classname}}Constraints(obj {{classname}}) error { {{/items.isModel}} {{/isArray}} {{/isNullable}} +{{/isReadOnly}} {{/Vars}} return nil }{{/model}}{{/models}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java index 9195a5859614..38190747fc49 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java @@ -74,6 +74,37 @@ public void verifyOrder() throws IOException { } + @Test + public void verifyReadOnlyRequiredFieldsNotEnforced() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = createDefaultCodegenConfigurator(output) + .setInputSpec("src/test/resources/3_0/go-server/readonly-required.yaml"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + // AssertThingRequired must not reference the readOnly "id" field, so a + // request body that legitimately omits it is accepted. The non-readOnly + // required "name" field must still be enforced. + java.nio.file.Path modelPath = Paths.get(output + "/go/model_thing.go"); + TestUtils.assertFileExists(modelPath); + TestUtils.assertFileContains(modelPath, + "func AssertThingRequired(obj Thing) error {"); + TestUtils.assertFileContains(modelPath, + "\"name\": obj.Name,"); + // readOnly "id" must be skipped + TestUtils.assertFileNotContains(modelPath, + "\"id\": obj.Id,"); + // readOnly nested model "meta" must not be recursed into, so a + // request body that omits it (and thus omits Meta's own required + // fields) is accepted. + TestUtils.assertFileNotContains(modelPath, + "if err := AssertMetaRequired(obj.Meta); err != nil {"); + } + private static CodegenConfigurator createDefaultCodegenConfigurator(File output) { return new CodegenConfigurator() .setGeneratorName("go-server") diff --git a/modules/openapi-generator/src/test/resources/3_0/go-server/readonly-required.yaml b/modules/openapi-generator/src/test/resources/3_0/go-server/readonly-required.yaml new file mode 100644 index 000000000000..1e0f23541b8c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/go-server/readonly-required.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.1 + +info: + version: 1.0.0 + title: readOnly required fields must not be enforced on request bodies + +paths: + /thing: + post: + operationId: createThing + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + responses: + '201': + description: created + +components: + schemas: + Thing: + type: object + properties: + id: + type: string + readOnly: true + name: + type: string + meta: + readOnly: true + allOf: + - $ref: '#/components/schemas/Meta' + required: + - id + - name + - meta + Meta: + type: object + properties: + createdBy: + type: string + required: + - createdBy