diff --git a/.github/workflows/samples-go.yaml b/.github/workflows/samples-go.yaml index edd397a2e8c6..7ce9d00384bf 100644 --- a/.github/workflows/samples-go.yaml +++ b/.github/workflows/samples-go.yaml @@ -27,6 +27,7 @@ jobs: - samples/server/petstore/go-chi-server/ - samples/server/others/go-server/no-body-path-params/ - samples/server/others/go-server/optional-body/ + - samples/server/others/go-server/required-zero-value/ steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v6 @@ -35,7 +36,7 @@ jobs: - run: go version - name: Run test working-directory: ${{ matrix.sample }} - run: go test -mod=mod -v + run: go test -mod=mod ./... -v verify: name: Verify generated Go files with Go tests diff --git a/bin/configs/go-server-required-zero-value.yaml b/bin/configs/go-server-required-zero-value.yaml new file mode 100644 index 000000000000..56eab29d5e56 --- /dev/null +++ b/bin/configs/go-server-required-zero-value.yaml @@ -0,0 +1,8 @@ +generatorName: go-server +outputDir: samples/server/others/go-server/required-zero-value +inputSpec: modules/openapi-generator/src/test/resources/3_0/go-server/required-zero-value.yaml +templateDir: modules/openapi-generator/src/main/resources/go-server +additionalProperties: + hideGenerationTimestamp: "true" + packageName: petstoreserver + addResponseHeaders: true diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoServerCodegen.java index f184024a7d9d..9356bb7347f2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoServerCodegen.java @@ -31,6 +31,7 @@ import java.io.File; import java.util.*; +import java.util.stream.Collectors; import static org.openapitools.codegen.utils.StringUtils.camelize; @@ -296,6 +297,86 @@ public void processOpts() { .doNotOverwrite()); } + @Override + public Map postProcessAllModels(Map objs) { + objs = super.postProcessAllModels(objs); + + for (ModelsMap modelsMap : objs.values()) { + List> imports = modelsMap.getImports(); + if (imports == null) { + imports = new ArrayList<>(); + modelsMap.setImports(imports); + } + for (ModelMap modelMap : modelsMap.getModels()) { + configureModelRequiredValidation(modelMap.getModel(), imports); + } + } + + return objs; + } + + /** + * Configures UnmarshalJSON presence checks and AssertRequired helpers for request-body models. + * Runs in {@link #postProcessAllModels} so {@code parentModel} is populated for allOf embedding. + */ + private void configureModelRequiredValidation(CodegenModel model, List> imports) { + if (model == null || model.isEnum) { + return; + } + + // Parent embedding must be wired regardless of required fields. + // Otherwise allOf child models without required properties silently + // drop their parent from the generated Go struct. + boolean hasEmbeddedParent = model.parentModel != null; + if (hasEmbeddedParent) { + model.vendorExtensions.put("x-go-embed-parent-classname", model.parentModel.classname); + } + model.vendorExtensions.put("x-go-has-parent", hasEmbeddedParent); + + Map decodeVarsByBaseName = new LinkedHashMap<>(); + for (CodegenProperty v : model.allVars) { + decodeVarsByBaseName.put(v.baseName, v); + } + for (CodegenProperty v : model.vars) { + decodeVarsByBaseName.put(v.baseName, v); + } + List decodeVars = new ArrayList<>(decodeVarsByBaseName.values()); + model.vendorExtensions.put("decodeVars", decodeVars); + + List presenceCheckRequiredVars = decodeVars.stream() + .filter(v -> v.required && !v.isReadOnly) + .collect(Collectors.toList()); + + model.vendorExtensions.put("presenceCheckRequiredVars", presenceCheckRequiredVars); + boolean hasPresenceCheckRequiredVars = !presenceCheckRequiredVars.isEmpty(); + + model.vendorExtensions.put("hasPresenceCheckRequiredVars", hasPresenceCheckRequiredVars); + + if (hasPresenceCheckRequiredVars) { + imports.add(createMapping("import", "encoding/json")); + if (!model.isAdditionalPropertiesTrue) { + imports.add(createMapping("import", "fmt")); + } + } + + List allowedJsonKeys = decodeVars.stream() + .map(v -> v.baseName) + .collect(Collectors.toList()); + model.vendorExtensions.put("allowedJsonKeys", allowedJsonKeys); + + boolean hasRequiredAssertVars = model.vars.stream() + .anyMatch(v -> v.required && !v.isReadOnly && !v.isNullable + && (v.isModel || v.isArray || v.isMap)); + model.vendorExtensions.put("hasRequiredAssertVars", hasRequiredAssertVars); + if (hasRequiredAssertVars) { + List assertRequiredVars = model.vars.stream() + .filter(v -> v.required && !v.isReadOnly && !v.isNullable + && (v.isModel || v.isArray || v.isMap)) + .collect(Collectors.toList()); + model.vendorExtensions.put("assertRequiredVars", assertRequiredVars); + } + } + @Override public ModelsMap postProcessModels(ModelsMap objs) { // The superclass determines the list of required golang imports. The actual list of imports diff --git a/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache b/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache index 41153b5addde..5aa2a17046d5 100644 --- a/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache @@ -5,9 +5,9 @@ package {{packageName}} import ( {{#hasBodyParams}} "encoding/json" + "errors" {{/hasBodyParams}} {{#hasOptionalBodyParams}} - "errors" "io" {{/hasOptionalBodyParams}} "net/http" @@ -567,6 +567,16 @@ func (c *{{classname}}Controller) {{operationId}}(w http.ResponseWriter, r *http } {{paramName}}Param = param + {{#isArray}} + defer func() { + for _, f := range param { + f.Close() + } + }() + {{/isArray}} + {{^isArray}} + defer param.Close() + {{/isArray}} } {{/isFile}} {{#isLong}}{{#isArray}} @@ -614,6 +624,11 @@ func (c *{{classname}}Controller) {{operationId}}(w http.ResponseWriter, r *http d.DisallowUnknownFields() {{/isAdditionalPropertiesTrue}} if err := d.Decode(&{{paramName}}Param); err != nil {{^required}}&& !errors.Is(err, io.EOF) {{/required}}{ + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/modules/openapi-generator/src/main/resources/go-server/error.mustache b/modules/openapi-generator/src/main/resources/go-server/error.mustache index 4c90a3b9f253..350f59795327 100644 --- a/modules/openapi-generator/src/main/resources/go-server/error.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/error.mustache @@ -36,7 +36,7 @@ type RequiredError struct { } func (e *RequiredError) Error() string { - return fmt.Sprintf("required field '%s' is zero value.", e.Field) + return fmt.Sprintf("field '%s' is required.", e.Field) } // ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if diff --git a/modules/openapi-generator/src/main/resources/go-server/helpers.mustache b/modules/openapi-generator/src/main/resources/go-server/helpers.mustache index b50fd0176d79..04948d38293a 100644 --- a/modules/openapi-generator/src/main/resources/go-server/helpers.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/helpers.mustache @@ -142,6 +142,10 @@ func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) { for _, fileHeader := range r.MultipartForm.File[key] { file, err := readFileHeaderToTempFile(fileHeader) if err != nil { + for _, f := range files { + f.Close() + os.Remove(f.Name()) + } return nil, err } @@ -167,13 +171,21 @@ func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error return nil, err } - defer file.Close() - _, err = io.Copy(file, formFile) if err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + // Rewind so the caller can read from the beginning. + // The caller is responsible for closing the returned file. + if _, err := file.Seek(0, io.SeekStart); err != nil { + file.Close() + os.Remove(file.Name()) return nil, err } - + return file, nil } 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 28dd5d3cb61e..4a38893652f2 100644 --- a/modules/openapi-generator/src/main/resources/go-server/model.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/model.mustache @@ -57,16 +57,9 @@ func New{{{classname}}}FromValue(v {{{format}}}{{^format}}{{dataType}}{{/format} {{/isEnum}}{{^isEnum}}{{#description}} // {{classname}} - {{{description}}}{{/description}} type {{classname}} struct { -{{#parent}} -{{^isMap}} -{{^isArray}} - {{{parent}}} -{{/isArray}} -{{/isMap}} -{{#isArray}} - Items {{{parent}}} -{{/isArray}} -{{/parent}} +{{#vendorExtensions.x-go-embed-parent-classname}} + {{{.}}} +{{/vendorExtensions.x-go-embed-parent-classname}} {{#vars}}{{#description}} // {{{.}}}{{/description}} {{#deprecated}} @@ -74,31 +67,36 @@ type {{classname}} struct { {{/deprecated}} {{name}} {{#isNullable}}*{{/isNullable}}{{{dataType}}} `json:"{{baseName}}{{^required}},omitempty{{/required}}"{{#vendorExtensions.x-go-custom-tag}} {{{.}}}{{/vendorExtensions.x-go-custom-tag}}` {{/vars}} -}{{/isEnum}} +} +{{#vendorExtensions.hasPresenceCheckRequiredVars}} +{{>model_unmarshal_json_presence}} +{{/vendorExtensions.hasPresenceCheckRequiredVars}} +{{/isEnum}} -// Assert{{classname}}Required checks if the required fields are not zero-ed +// Assert{{classname}}Required checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func Assert{{classname}}Required(obj {{classname}}) error { {{#hasRequired}} +{{#vendorExtensions.hasRequiredAssertVars}} elements := map[string]interface{}{ -{{#requiredVars}}{{^isReadOnly}} "{{baseName}}": obj.{{name}}, -{{/isReadOnly}}{{/requiredVars}} } +{{#vendorExtensions.assertRequiredVars}} + "{{baseName}}": obj.{{name}}, +{{/vendorExtensions.assertRequiredVars}} + } for name, el := range elements { if isZero := IsZeroValue(el); isZero { return &RequiredError{Field: name} } } +{{/vendorExtensions.hasRequiredAssertVars}} {{/hasRequired}} -{{#parent}} - {{^isMap}} - {{^isArray}} - if err := Assert{{{parent}}}Required(obj.{{{parent}}}); err != nil { +{{#vendorExtensions.x-go-embed-parent-classname}} + if err := Assert{{vendorExtensions.x-go-embed-parent-classname}}Required(obj.{{vendorExtensions.x-go-embed-parent-classname}}); err != nil { return err } - {{/isArray}} - {{/isMap}} -{{/parent}} +{{/vendorExtensions.x-go-embed-parent-classname}} {{#Vars}} {{^isReadOnly}} {{#isNullable}} diff --git a/modules/openapi-generator/src/main/resources/go-server/model_unmarshal_json_presence.mustache b/modules/openapi-generator/src/main/resources/go-server/model_unmarshal_json_presence.mustache new file mode 100644 index 000000000000..2f82e6c67dcf --- /dev/null +++ b/modules/openapi-generator/src/main/resources/go-server/model_unmarshal_json_presence.mustache @@ -0,0 +1,64 @@ +// UnmarshalJSON validates required property keys then unmarshals into {{classname}} +func (o *{{classname}}) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ +{{#vendorExtensions.presenceCheckRequiredVars}} + "{{baseName}}", +{{/vendorExtensions.presenceCheckRequiredVars}} + } + + requiredNullableProperties := map[string]bool{ +{{#vendorExtensions.presenceCheckRequiredVars}} + "{{baseName}}": {{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}, +{{/vendorExtensions.presenceCheckRequiredVars}} + } + + {{^isAdditionalPropertiesTrue}} + allowedJsonKeys := map[string]struct{}{ +{{#vendorExtensions.allowedJsonKeys}} + "{{.}}": {}, +{{/vendorExtensions.allowedJsonKeys}} + } + + {{/isAdditionalPropertiesTrue}} + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + {{^isAdditionalPropertiesTrue}} + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + {{/isAdditionalPropertiesTrue}} + var decoded {{classname}} + +{{#vendorExtensions.decodeVars}} + if value, exists := allProperties["{{baseName}}"]; exists { + if err = json.Unmarshal(value, &decoded.{{name}}); err != nil { + return err + } + } +{{/vendorExtensions.decodeVars}} + + *o = decoded + + return nil +} 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 758632277087..a773d05b3364 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 @@ -127,6 +127,108 @@ public void verifyDuplicateOperationIdsAreConsistent() throws IOException { "nickname diverged from operationId (FooGet_1 present in api_default_service.go)"); } + @Test + public void verifyRequiredZeroValueAllowed() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = createDefaultCodegenConfigurator(output) + .setInputSpec("src/test/resources/3_0/go-server/required-zero-value.yaml"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + TestUtils.assertFileExists(Paths.get(output + "/go/model_test_object.go")); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "func (o *TestObject) UnmarshalJSON(data []byte) (err error) {"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "var decoded TestObject"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "requiredProperties := []string{"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "allowedJsonKeys := map[string]struct{}{"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "return fmt.Errorf(\"json: unknown field %q\", key)"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "json.Unmarshal(value, &decoded.N)"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "json.Unmarshal(value, &decoded.Name)"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "return &RequiredError{Field: requiredProperty}"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "if string(value) == \"null\" && !requiredNullableProperties[requiredProperty] {"); + TestUtils.assertFileNotContains(Paths.get(output + "/go/model_test_object.go"), + "IsZeroValue"); + + TestUtils.assertFileExists(Paths.get(output + "/go/api_default.go")); + TestUtils.assertFileContains(Paths.get(output + "/go/api_default.go"), + "var requiredErr *RequiredError"); + } + + @Test + public void verifyOneOfInlineRequiredZeroValueAllowed() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = createDefaultCodegenConfigurator(output) + .setInputSpec("src/test/resources/3_0/go-server/oneof-inline-required.yaml"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + java.nio.file.Path modelPath = Paths.get(output + "/go/model_item_request.go"); + TestUtils.assertFileExists(modelPath); + TestUtils.assertFileContains(modelPath, + "func (o *ItemRequest) UnmarshalJSON(data []byte) (err error) {"); + TestUtils.assertFileContains(modelPath, + "\"quantity\","); + TestUtils.assertFileNotContains(modelPath, + "\"quantity\": obj.Quantity,"); + } + + @Test + public void verifyOrderEmbedsAllOfParent() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = createDefaultCodegenConfigurator(output) + .setInputSpec("src/test/resources/3_0/go-server/petstore_with_test_endpoint.yaml"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + java.nio.file.Path modelPath = Paths.get(output + "/go/model_order.go"); + TestUtils.assertFileExists(modelPath); + TestUtils.assertFileContains(modelPath, "SpecialInfo"); + TestUtils.assertFileContains(modelPath, "\"requireTest\","); + TestUtils.assertFileContains(modelPath, "json.Unmarshal(value, &decoded.PetId)"); + TestUtils.assertFileContains(modelPath, "json.Unmarshal(value, &decoded.RequireTest)"); + } + + @Test + public void verifyOrderUnmarshalJSONHandlesInheritedAllOfRequiredFields() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = createDefaultCodegenConfigurator(output) + .setInputSpec("src/test/resources/3_0/go-server/petstore_with_test_endpoint.yaml") + .addAdditionalProperty("router", "chi"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + java.nio.file.Path modelPath = Paths.get(output + "/go/model_order.go"); + TestUtils.assertFileExists(modelPath); + TestUtils.assertFileContains(modelPath, "\"comment\","); + TestUtils.assertFileContains(modelPath, "\"requireTest\","); + TestUtils.assertFileContains(modelPath, "json.Unmarshal(value, &decoded.Comment)"); + TestUtils.assertFileContains(modelPath, "json.Unmarshal(value, &decoded.RequireTest)"); + } + private static CodegenConfigurator createDefaultCodegenConfigurator(File output) { return new CodegenConfigurator() .setGeneratorName("go-server") @@ -149,13 +251,16 @@ public void verifyReadOnlyRequiredFieldsNotEnforced() throws IOException { 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. + // request body that legitimately omits it is accepted. + // Primitive non-readOnly required fields are validated in UnmarshalJSON, not here. 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, + "Primitive required fields are validated for JSON request bodies in UnmarshalJSON"); + // name is a primitive string, validated by UnmarshalJSON — excluded from AssertRequired + TestUtils.assertFileNotContains(modelPath, "\"name\": obj.Name,"); // readOnly "id" must be skipped TestUtils.assertFileNotContains(modelPath, diff --git a/modules/openapi-generator/src/test/resources/3_0/go-server/oneof-inline-required.yaml b/modules/openapi-generator/src/test/resources/3_0/go-server/oneof-inline-required.yaml new file mode 100644 index 000000000000..388cb0596706 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/go-server/oneof-inline-required.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.3 +info: + title: oneOf inline required zero value + version: 1.0.0 +servers: + - url: https://localhost:8080/api/v1 +paths: + /items: + post: + summary: Create item + operationId: createItem + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemRequest' + responses: + '204': + description: Successful operation +components: + schemas: + NestedLabel: + type: object + properties: + name: + type: string + required: + - name + ItemRequest: + oneOf: + - $ref: '#/components/schemas/NestedLabel' + - type: string + properties: + quantity: + type: integer + minimum: 0 + required: + - quantity diff --git a/modules/openapi-generator/src/test/resources/3_0/go-server/required-zero-value.yaml b/modules/openapi-generator/src/test/resources/3_0/go-server/required-zero-value.yaml new file mode 100644 index 000000000000..cc35364db18e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/go-server/required-zero-value.yaml @@ -0,0 +1,43 @@ +openapi: 3.0.3 +info: + title: Test - OpenAPI 3.0 + version: 1.0.0 +servers: + - url: https://localhost:8080/api/v1 +paths: + /config: + put: + summary: Update something + operationId: update + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestObject' + required: true + responses: + '204': + description: Successful operation +components: + schemas: + BaseObject: + type: object + discriminator: + propertyName: kind + properties: + kind: + type: string + name: + type: string + required: + - name + TestObject: + allOf: + - $ref: '#/components/schemas/BaseObject' + - type: object + properties: + n: + type: integer + minimum: 0 + required: + - n diff --git a/samples/openapi3/server/petstore/go/api_store_test.go b/samples/openapi3/server/petstore/go/api_store_test.go index ed6ab3dbbe36..61c057c2f265 100644 --- a/samples/openapi3/server/petstore/go/api_store_test.go +++ b/samples/openapi3/server/petstore/go/api_store_test.go @@ -35,7 +35,7 @@ func TestPlaceOrderOK(t *testing.T) { "shipDate": "2000-01-23T04:56:07.000+00:00", "complete": false, "status": "placed", - "RequireTest": "required" + "requireTest": "required" }` status = http.StatusOK ) @@ -78,8 +78,7 @@ func TestPlaceOrderOK(t *testing.T) { assert.Empty(t, data) } -// TestPlaceOrderFailEmbeddedRequired tests PlaceOrder operation. This operation contains Order which has an -// embedding struct that will be tested as well. +// TestPlaceOrderFailEmbeddedRequired tests PlaceOrder rejects a request body missing an inherited allOf required field. func TestPlaceOrderFailEmbeddedRequired(t *testing.T) { const ( payload = `{ @@ -121,5 +120,5 @@ func TestPlaceOrderFailEmbeddedRequired(t *testing.T) { // Check the response data, err := io.ReadAll(res.Body) require.NoError(t, err) - assert.Equal(t, "\"required field 'requireTest' is zero value.\"\n", string(data)) + assert.Equal(t, "\"field 'requireTest' is required.\"\n", string(data)) } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/api_pet.go b/samples/openapi3/server/petstore/go/go-petstore/go/api_pet.go index 64096d02826c..a2dc78b0947c 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/api_pet.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/api_pet.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" "os" @@ -165,6 +166,11 @@ func (c *PetAPIController) UpdatePet(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&petParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -192,6 +198,11 @@ func (c *PetAPIController) AddPet(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&petParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -353,6 +364,7 @@ func (c *PetAPIController) UploadFile(w http.ResponseWriter, r *http.Request) { } fileParam = param + defer param.Close() } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/api_store.go b/samples/openapi3/server/petstore/go/go-petstore/go/api_store.go index f7144203bf64..19084447a7ea 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/api_store.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/api_store.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" @@ -128,6 +129,11 @@ func (c *StoreAPIController) PlaceOrder(w http.ResponseWriter, r *http.Request) d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&orderParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/api_user.go b/samples/openapi3/server/petstore/go/go-petstore/go/api_user.go index 5f3681af879e..9ff9dbd45880 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/api_user.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/api_user.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" @@ -164,6 +165,11 @@ func (c *UserAPIController) CreateUser(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -191,6 +197,11 @@ func (c *UserAPIController) CreateUsersWithArrayInput(w http.ResponseWriter, r * d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -216,6 +227,11 @@ func (c *UserAPIController) CreateUsersWithListInput(w http.ResponseWriter, r *h d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -380,6 +396,11 @@ func (c *UserAPIController) UpdateUser(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/error.go b/samples/openapi3/server/petstore/go/go-petstore/go/error.go index ba23f89b8385..9f4f1744dae4 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/error.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/error.go @@ -45,7 +45,7 @@ type RequiredError struct { } func (e *RequiredError) Error() string { - return fmt.Sprintf("required field '%s' is zero value.", e.Field) + return fmt.Sprintf("field '%s' is required.", e.Field) } // ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/helpers.go b/samples/openapi3/server/petstore/go/go-petstore/go/helpers.go index 6f0e318d8e1f..3ba5a5fbc40e 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/helpers.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/helpers.go @@ -145,6 +145,10 @@ func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) { for _, fileHeader := range r.MultipartForm.File[key] { file, err := readFileHeaderToTempFile(fileHeader) if err != nil { + for _, f := range files { + f.Close() + os.Remove(f.Name()) + } return nil, err } @@ -170,13 +174,21 @@ func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error return nil, err } - defer file.Close() - _, err = io.Copy(file, formFile) if err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + // Rewind so the caller can read from the beginning. + // The caller is responsible for closing the returned file. + if _, err := file.Seek(0, io.SeekStart); err != nil { + file.Close() + os.Remove(file.Name()) return nil, err } - + return file, nil } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_an_object.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_an_object.go index b5b8e6c5276f..288bc16be8c1 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_an_object.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_an_object.go @@ -22,7 +22,8 @@ type AnObject struct { Pet []Pet `json:"Pet,omitempty"` } -// AssertAnObjectRequired checks if the required fields are not zero-ed +// AssertAnObjectRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertAnObjectRequired(obj AnObject) error { if err := AssertTagRequired(obj.Tag); err != nil { return err diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_api_response.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_api_response.go index c6bbed4687a3..42ff7e66467f 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_api_response.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_api_response.go @@ -23,7 +23,8 @@ type ApiResponse struct { Message string `json:"message,omitempty"` } -// AssertApiResponseRequired checks if the required fields are not zero-ed +// AssertApiResponseRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertApiResponseRequired(obj ApiResponse) error { return nil } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_category.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_category.go index a8c835c0da4d..b8e36a206e55 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_category.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_category.go @@ -21,7 +21,8 @@ type Category struct { Name string `json:"name,omitempty" validate:"regexp=^[a-zA-Z0-9]+[a-zA-Z0-9\\\\.\\\\-_]*[a-zA-Z0-9]+$"` } -// AssertCategoryRequired checks if the required fields are not zero-ed +// AssertCategoryRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertCategoryRequired(obj Category) error { return nil } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_order.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_order.go index f5a9b8f281be..dc1511473ad2 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_order.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_order.go @@ -13,11 +13,14 @@ package petstoreserver import ( "time" + "encoding/json" + "fmt" ) type Order struct { + SpecialInfo PetId int64 `json:"petId,omitempty"` @@ -34,18 +37,121 @@ type Order struct { Comment *string `json:"comment"` } +// UnmarshalJSON validates required property keys then unmarshals into Order +func (o *Order) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "requireTest", + "comment", + } -// AssertOrderRequired checks if the required fields are not zero-ed -func AssertOrderRequired(obj Order) error { - elements := map[string]interface{}{ - "comment": obj.Comment, - "requireTest": obj.RequireTest, + requiredNullableProperties := map[string]bool{ + "requireTest": false, + "comment": true, + } + + allowedJsonKeys := map[string]struct{}{ + "petId": {}, + "quantity": {}, + "shipDate": {}, + "promotion": {}, + "requireTest": {}, + "type": {}, + "id": {}, + "status": {}, + "complete": {}, + "comment": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded Order + + if value, exists := allProperties["petId"]; exists { + if err = json.Unmarshal(value, &decoded.PetId); err != nil { + return err + } + } + if value, exists := allProperties["quantity"]; exists { + if err = json.Unmarshal(value, &decoded.Quantity); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["shipDate"]; exists { + if err = json.Unmarshal(value, &decoded.ShipDate); err != nil { + return err } } + if value, exists := allProperties["promotion"]; exists { + if err = json.Unmarshal(value, &decoded.Promotion); err != nil { + return err + } + } + if value, exists := allProperties["requireTest"]; exists { + if err = json.Unmarshal(value, &decoded.RequireTest); err != nil { + return err + } + } + if value, exists := allProperties["type"]; exists { + if err = json.Unmarshal(value, &decoded.Type); err != nil { + return err + } + } + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["status"]; exists { + if err = json.Unmarshal(value, &decoded.Status); err != nil { + return err + } + } + if value, exists := allProperties["complete"]; exists { + if err = json.Unmarshal(value, &decoded.Complete); err != nil { + return err + } + } + if value, exists := allProperties["comment"]; exists { + if err = json.Unmarshal(value, &decoded.Comment); err != nil { + return err + } + } + + *o = decoded + + return nil +} + +// AssertOrderRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertOrderRequired(obj Order) error { + if err := AssertSpecialInfoRequired(obj.SpecialInfo); err != nil { + return err + } return nil } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_order_info.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_order_info.go index 21f5d24946c2..4e1000ff7634 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_order_info.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_order_info.go @@ -27,7 +27,8 @@ type OrderInfo struct { ShipDate time.Time `json:"shipDate,omitempty"` } -// AssertOrderInfoRequired checks if the required fields are not zero-ed +// AssertOrderInfoRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertOrderInfoRequired(obj OrderInfo) error { return nil } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_pet.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_pet.go index 38a5af1d3935..5f8e0e5b4277 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_pet.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_pet.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // Pet - A pet for sale in the pet store @@ -30,19 +35,94 @@ type Pet struct { // Deprecated Status string `json:"status,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into Pet +func (o *Pet) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "name", + "photoUrls", + } -// AssertPetRequired checks if the required fields are not zero-ed -func AssertPetRequired(obj Pet) error { - elements := map[string]interface{}{ - "name": obj.Name, - "photoUrls": obj.PhotoUrls, + requiredNullableProperties := map[string]bool{ + "name": false, + "photoUrls": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "category": {}, + "name": {}, + "photoUrls": {}, + "tags": {}, + "status": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) } } + var decoded Pet + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["category"]; exists { + if err = json.Unmarshal(value, &decoded.Category); err != nil { + return err + } + } + if value, exists := allProperties["name"]; exists { + if err = json.Unmarshal(value, &decoded.Name); err != nil { + return err + } + } + if value, exists := allProperties["photoUrls"]; exists { + if err = json.Unmarshal(value, &decoded.PhotoUrls); err != nil { + return err + } + } + if value, exists := allProperties["tags"]; exists { + if err = json.Unmarshal(value, &decoded.Tags); err != nil { + return err + } + } + if value, exists := allProperties["status"]; exists { + if err = json.Unmarshal(value, &decoded.Status); err != nil { + return err + } + } + + *o = decoded + + return nil +} + +// AssertPetRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertPetRequired(obj Pet) error { if obj.Category != nil { if err := AssertCategoryRequired(*obj.Category); err != nil { return err diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_special_info.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_special_info.go index c09677118547..82dd11c7d561 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_special_info.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_special_info.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // SpecialInfo - An order info for a pets from the pet store @@ -22,18 +27,74 @@ type SpecialInfo struct { Type string `json:"type,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into SpecialInfo +func (o *SpecialInfo) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "requireTest", + } -// AssertSpecialInfoRequired checks if the required fields are not zero-ed -func AssertSpecialInfoRequired(obj SpecialInfo) error { - elements := map[string]interface{}{ - "requireTest": obj.RequireTest, + requiredNullableProperties := map[string]bool{ + "requireTest": false, + } + + allowedJsonKeys := map[string]struct{}{ + "promotion": {}, + "requireTest": {}, + "type": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) } } + var decoded SpecialInfo + + if value, exists := allProperties["promotion"]; exists { + if err = json.Unmarshal(value, &decoded.Promotion); err != nil { + return err + } + } + if value, exists := allProperties["requireTest"]; exists { + if err = json.Unmarshal(value, &decoded.RequireTest); err != nil { + return err + } + } + if value, exists := allProperties["type"]; exists { + if err = json.Unmarshal(value, &decoded.Type); err != nil { + return err + } + } + + *o = decoded + + return nil +} + +// AssertSpecialInfoRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertSpecialInfoRequired(obj SpecialInfo) error { return nil } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_tag.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_tag.go index 88fb735f34cc..3c83700976ca 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_tag.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_tag.go @@ -21,7 +21,8 @@ type Tag struct { Name string `json:"name,omitempty"` } -// AssertTagRequired checks if the required fields are not zero-ed +// AssertTagRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertTagRequired(obj Tag) error { return nil } diff --git a/samples/openapi3/server/petstore/go/go-petstore/go/model_user.go b/samples/openapi3/server/petstore/go/go-petstore/go/model_user.go index 29803083edbf..0ee8cbca0c24 100644 --- a/samples/openapi3/server/petstore/go/go-petstore/go/model_user.go +++ b/samples/openapi3/server/petstore/go/go-petstore/go/model_user.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // User - A User who is purchasing from the pet store @@ -39,18 +44,116 @@ type User struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into User +func (o *User) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "deepSliceModel", + } -// AssertUserRequired checks if the required fields are not zero-ed -func AssertUserRequired(obj User) error { - elements := map[string]interface{}{ - "deepSliceModel": obj.DeepSliceModel, + requiredNullableProperties := map[string]bool{ + "deepSliceModel": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "username": {}, + "firstName": {}, + "lastName": {}, + "email": {}, + "password": {}, + "phone": {}, + "userStatus": {}, + "deepSliceModel": {}, + "deepSliceMap": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded User + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["username"]; exists { + if err = json.Unmarshal(value, &decoded.Username); err != nil { + return err + } + } + if value, exists := allProperties["firstName"]; exists { + if err = json.Unmarshal(value, &decoded.FirstName); err != nil { + return err + } + } + if value, exists := allProperties["lastName"]; exists { + if err = json.Unmarshal(value, &decoded.LastName); err != nil { + return err + } + } + if value, exists := allProperties["email"]; exists { + if err = json.Unmarshal(value, &decoded.Email); err != nil { + return err + } + } + if value, exists := allProperties["password"]; exists { + if err = json.Unmarshal(value, &decoded.Password); err != nil { + return err + } + } + if value, exists := allProperties["phone"]; exists { + if err = json.Unmarshal(value, &decoded.Phone); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["userStatus"]; exists { + if err = json.Unmarshal(value, &decoded.UserStatus); err != nil { + return err } } + if value, exists := allProperties["deepSliceModel"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceModel); err != nil { + return err + } + } + if value, exists := allProperties["deepSliceMap"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceMap); err != nil { + return err + } + } + + *o = decoded + + return nil +} +// AssertUserRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertUserRequired(obj User) error { if obj.DeepSliceModel != nil { if err := AssertRecurseInterfaceRequired(*obj.DeepSliceModel, AssertTagRequired); err != nil { return err diff --git a/samples/server/others/go-server/no-body-path-params/go/api_body.go b/samples/server/others/go-server/no-body-path-params/go/api_body.go index cd4c3d6384ae..d29e10afe7d3 100644 --- a/samples/server/others/go-server/no-body-path-params/go/api_body.go +++ b/samples/server/others/go-server/no-body-path-params/go/api_body.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" ) @@ -78,6 +79,11 @@ func (c *BodyAPIController) Body(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&bodyRequestParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/others/go-server/no-body-path-params/go/api_both.go b/samples/server/others/go-server/no-body-path-params/go/api_both.go index f32acf67c529..b144ee6f0291 100644 --- a/samples/server/others/go-server/no-body-path-params/go/api_both.go +++ b/samples/server/others/go-server/no-body-path-params/go/api_both.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" @@ -86,6 +87,11 @@ func (c *BothAPIController) Both(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&bodyRequestParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/others/go-server/no-body-path-params/go/error.go b/samples/server/others/go-server/no-body-path-params/go/error.go index 6da8e9adacba..558e9cd7eebe 100644 --- a/samples/server/others/go-server/no-body-path-params/go/error.go +++ b/samples/server/others/go-server/no-body-path-params/go/error.go @@ -45,7 +45,7 @@ type RequiredError struct { } func (e *RequiredError) Error() string { - return fmt.Sprintf("required field '%s' is zero value.", e.Field) + return fmt.Sprintf("field '%s' is required.", e.Field) } // ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if diff --git a/samples/server/others/go-server/no-body-path-params/go/helpers.go b/samples/server/others/go-server/no-body-path-params/go/helpers.go index a4df01617325..e760f904d7ed 100644 --- a/samples/server/others/go-server/no-body-path-params/go/helpers.go +++ b/samples/server/others/go-server/no-body-path-params/go/helpers.go @@ -145,6 +145,10 @@ func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) { for _, fileHeader := range r.MultipartForm.File[key] { file, err := readFileHeaderToTempFile(fileHeader) if err != nil { + for _, f := range files { + f.Close() + os.Remove(f.Name()) + } return nil, err } @@ -170,13 +174,21 @@ func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error return nil, err } - defer file.Close() - _, err = io.Copy(file, formFile) if err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + // Rewind so the caller can read from the beginning. + // The caller is responsible for closing the returned file. + if _, err := file.Seek(0, io.SeekStart); err != nil { + file.Close() + os.Remove(file.Name()) return nil, err } - + return file, nil } diff --git a/samples/server/others/go-server/no-body-path-params/go/model_body_request.go b/samples/server/others/go-server/no-body-path-params/go/model_body_request.go index 92b220c01b2f..f6f5fd5ba337 100644 --- a/samples/server/others/go-server/no-body-path-params/go/model_body_request.go +++ b/samples/server/others/go-server/no-body-path-params/go/model_body_request.go @@ -18,7 +18,8 @@ type BodyRequest struct { Param string `json:"param,omitempty"` } -// AssertBodyRequestRequired checks if the required fields are not zero-ed +// AssertBodyRequestRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertBodyRequestRequired(obj BodyRequest) error { return nil } diff --git a/samples/server/others/go-server/optional-body/go/api_just_api.go b/samples/server/others/go-server/optional-body/go/api_just_api.go index 86181fe26f09..7f93688c0412 100644 --- a/samples/server/others/go-server/optional-body/go/api_just_api.go +++ b/samples/server/others/go-server/optional-body/go/api_just_api.go @@ -80,6 +80,11 @@ func (c *JustApiAPIController) SendOptionalPayload(w http.ResponseWriter, r *htt d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&payloadParam); err != nil && !errors.Is(err, io.EOF) { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/others/go-server/optional-body/go/error.go b/samples/server/others/go-server/optional-body/go/error.go index 6a1d17d783c5..3ebb1239803c 100644 --- a/samples/server/others/go-server/optional-body/go/error.go +++ b/samples/server/others/go-server/optional-body/go/error.go @@ -45,7 +45,7 @@ type RequiredError struct { } func (e *RequiredError) Error() string { - return fmt.Sprintf("required field '%s' is zero value.", e.Field) + return fmt.Sprintf("field '%s' is required.", e.Field) } // ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if diff --git a/samples/server/others/go-server/optional-body/go/helpers.go b/samples/server/others/go-server/optional-body/go/helpers.go index cf8d1f5ae9fe..e8769e7c02e5 100644 --- a/samples/server/others/go-server/optional-body/go/helpers.go +++ b/samples/server/others/go-server/optional-body/go/helpers.go @@ -145,6 +145,10 @@ func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) { for _, fileHeader := range r.MultipartForm.File[key] { file, err := readFileHeaderToTempFile(fileHeader) if err != nil { + for _, f := range files { + f.Close() + os.Remove(f.Name()) + } return nil, err } @@ -170,13 +174,21 @@ func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error return nil, err } - defer file.Close() - _, err = io.Copy(file, formFile) if err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + // Rewind so the caller can read from the beginning. + // The caller is responsible for closing the returned file. + if _, err := file.Seek(0, io.SeekStart); err != nil { + file.Close() + os.Remove(file.Name()) return nil, err } - + return file, nil } diff --git a/samples/server/others/go-server/optional-body/go/model_payload.go b/samples/server/others/go-server/optional-body/go/model_payload.go index 89ce13937f85..3947f82ccb68 100644 --- a/samples/server/others/go-server/optional-body/go/model_payload.go +++ b/samples/server/others/go-server/optional-body/go/model_payload.go @@ -19,7 +19,8 @@ type Payload struct { Token string `json:"token,omitempty"` } -// AssertPayloadRequired checks if the required fields are not zero-ed +// AssertPayloadRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertPayloadRequired(obj Payload) error { return nil } diff --git a/samples/server/others/go-server/required-zero-value/.openapi-generator-ignore b/samples/server/others/go-server/required-zero-value/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/others/go-server/required-zero-value/.openapi-generator/FILES b/samples/server/others/go-server/required-zero-value/.openapi-generator/FILES new file mode 100644 index 000000000000..f0b3889baf78 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/.openapi-generator/FILES @@ -0,0 +1,15 @@ +Dockerfile +README.md +api/openapi.yaml +go.mod +go/api.go +go/api_default.go +go/api_default_service.go +go/error.go +go/helpers.go +go/impl.go +go/logger.go +go/model_base_object.go +go/model_test_object.go +go/routers.go +main.go diff --git a/samples/server/others/go-server/required-zero-value/.openapi-generator/VERSION b/samples/server/others/go-server/required-zero-value/.openapi-generator/VERSION new file mode 100644 index 000000000000..186c33c96ed8 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.24.0-SNAPSHOT diff --git a/samples/server/others/go-server/required-zero-value/Dockerfile b/samples/server/others/go-server/required-zero-value/Dockerfile new file mode 100644 index 000000000000..2a71bb776298 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.19 AS build +WORKDIR /go/src +COPY go ./go +COPY main.go . +COPY go.sum . +COPY go.mod . + +ENV CGO_ENABLED=0 + +RUN go build -o petstoreserver . + +FROM scratch AS runtime +COPY --from=build /go/src/petstoreserver ./ +EXPOSE 8080/tcp +ENTRYPOINT ["./petstoreserver"] diff --git a/samples/server/others/go-server/required-zero-value/README.md b/samples/server/others/go-server/required-zero-value/README.md new file mode 100644 index 000000000000..0dfc6878110c --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/README.md @@ -0,0 +1,35 @@ +# Go API Server for petstoreserver + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +## Overview +This server was generated by the [openapi-generator] +(https://openapi-generator.tech) project. +By using the [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote server, you can easily generate a server stub. + +To see how to make this your own, look here: + +[README](https://openapi-generator.tech) + +- API version: 1.0.0 +- Generator version: 7.24.0-SNAPSHOT + + +### Running the server +To run the server, follow these simple steps: + +``` +go run main.go +``` + +The server will be available on `http://localhost:8080`. + +To run the server in a docker container +``` +docker build --network=host -t petstoreserver . +``` + +Once image is built use +``` +docker run --rm -it petstoreserver +``` diff --git a/samples/server/others/go-server/required-zero-value/api/openapi.yaml b/samples/server/others/go-server/required-zero-value/api/openapi.yaml new file mode 100644 index 000000000000..5ebcec842475 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/api/openapi.yaml @@ -0,0 +1,47 @@ +openapi: 3.0.3 +info: + title: Test - OpenAPI 3.0 + version: 1.0.0 +servers: +- url: https://localhost:8080/api/v1 +paths: + /config: + put: + operationId: update + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TestObject" + required: true + responses: + "204": + description: Successful operation + summary: Update something +components: + schemas: + BaseObject: + discriminator: + propertyName: kind + properties: + kind: + type: string + name: + type: string + required: + - name + type: object + TestObject: + allOf: + - $ref: "#/components/schemas/BaseObject" + - properties: + "n": + minimum: 0 + type: integer + required: + - "n" + type: object + example: + kind: kind + name: name + "n": 0 diff --git a/samples/server/others/go-server/required-zero-value/go.mod b/samples/server/others/go-server/required-zero-value/go.mod new file mode 100644 index 000000000000..f71ce9c8223d --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go.mod @@ -0,0 +1,4 @@ +module github.com/GIT_USER_ID/GIT_REPO_ID + +go 1.18 + diff --git a/samples/server/others/go-server/required-zero-value/go/api.go b/samples/server/others/go-server/required-zero-value/go/api.go new file mode 100644 index 000000000000..76aea4115e87 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api.go @@ -0,0 +1,34 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +import ( + "context" + "net/http" +) + + + +// DefaultAPIRouter defines the required methods for binding the api requests to a responses for the DefaultAPI +// The DefaultAPIRouter implementation should parse necessary information from the http request, +// pass the data to a DefaultAPIServicer to perform the required actions, then write the service results to the http response. +type DefaultAPIRouter interface { + Update(http.ResponseWriter, *http.Request) +} + + +// DefaultAPIServicer defines the api actions for the DefaultAPI service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can be ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type DefaultAPIServicer interface { + Update(context.Context, TestObject) (ImplResponse, error) +} diff --git a/samples/server/others/go-server/required-zero-value/go/api_default.go b/samples/server/others/go-server/required-zero-value/go/api_default.go new file mode 100644 index 000000000000..849e3b08f592 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api_default.go @@ -0,0 +1,106 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +import ( + "encoding/json" + "errors" + "net/http" + "strings" +) + +// DefaultAPIController binds http requests to an api service and writes the service results to the http response +type DefaultAPIController struct { + service DefaultAPIServicer + errorHandler ErrorHandler +} + +// DefaultAPIOption for how the controller is set up. +type DefaultAPIOption func(*DefaultAPIController) + +// WithDefaultAPIErrorHandler inject ErrorHandler into controller +func WithDefaultAPIErrorHandler(h ErrorHandler) DefaultAPIOption { + return func(c *DefaultAPIController) { + c.errorHandler = h + } +} + +// NewDefaultAPIController creates a default api controller +func NewDefaultAPIController(s DefaultAPIServicer, opts ...DefaultAPIOption) *DefaultAPIController { + controller := &DefaultAPIController{ + service: s, + errorHandler: DefaultErrorHandler, + } + + for _, opt := range opts { + opt(controller) + } + + return controller +} + +// Routes returns all the api routes for the DefaultAPIController +func (c *DefaultAPIController) Routes() Routes { + return Routes{ + "Update": Route{ + "Update", + strings.ToUpper("Put"), + "/api/v1/config", + c.Update, + }, + } +} + +// OrderedRoutes returns all the api routes in a deterministic order for the DefaultAPIController +func (c *DefaultAPIController) OrderedRoutes() []Route { + return []Route{ + Route{ + "Update", + strings.ToUpper("Put"), + "/api/v1/config", + c.Update, + }, + } +} + + + +// Update - Update something +func (c *DefaultAPIController) Update(w http.ResponseWriter, r *http.Request) { + var testObjectParam TestObject + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&testObjectParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertTestObjectRequired(testObjectParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + if err := AssertTestObjectConstraints(testObjectParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.Update(r.Context(), testObjectParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + _ = EncodeJSONResponse(result.Body, &result.Code, result.Headers, w) +} diff --git a/samples/server/others/go-server/required-zero-value/go/api_default_service.go b/samples/server/others/go-server/required-zero-value/go/api_default_service.go new file mode 100644 index 000000000000..8a41ddee4ab2 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api_default_service.go @@ -0,0 +1,39 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +import ( + "context" + "net/http" + "errors" +) + +// DefaultAPIService is a service that implements the logic for the DefaultAPIServicer +// This service should implement the business logic for every endpoint for the DefaultAPI API. +// Include any external packages or services that will be required by this service. +type DefaultAPIService struct { +} + +// NewDefaultAPIService creates a default api service +func NewDefaultAPIService() *DefaultAPIService { + return &DefaultAPIService{} +} + +// Update - Update something +func (s *DefaultAPIService) Update(ctx context.Context, testObject TestObject) (ImplResponse, error) { + // TODO - update Update with the required logic for this service method. + // Add api_default_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + // TODO: Uncomment the next line to return response Response(204, {}) or use other options such as http.Ok ... + // return Response(204, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("Update method not implemented") +} diff --git a/samples/server/others/go-server/required-zero-value/go/api_default_test.go b/samples/server/others/go-server/required-zero-value/go/api_default_test.go new file mode 100644 index 000000000000..4ff276efbc5f --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api_default_test.go @@ -0,0 +1,113 @@ +package petstoreserver + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type DefaultAPITestService struct { + DefaultAPIServicer + update func(testObject TestObject) (ImplResponse, error) +} + +func (s *DefaultAPITestService) Update(ctx context.Context, testObject TestObject) (ImplResponse, error) { + return s.update(testObject) +} + +func TestUpdateAcceptsRequiredZeroValue(t *testing.T) { + const ( + payload = `{"name":"","n":0}` + status = http.StatusNoContent + ) + + req := httptest.NewRequest(http.MethodPut, "/config", strings.NewReader(payload)) + w := httptest.NewRecorder() + + service := &DefaultAPITestService{} + service.update = func(testObject TestObject) (ImplResponse, error) { + assert.Equal(t, "", testObject.Name) + assert.Equal(t, int32(0), testObject.N) + return Response(status, nil), nil + } + + controller := NewDefaultAPIController(service) + controller.Update(w, req) + + res := w.Result() + assert.Equal(t, status, res.StatusCode) + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Empty(t, data) +} + +func TestUpdateRejectsMissingRequiredField(t *testing.T) { + const ( + payload = `{"name":""}` + status = http.StatusUnprocessableEntity + ) + + req := httptest.NewRequest(http.MethodPut, "/config", strings.NewReader(payload)) + w := httptest.NewRecorder() + + service := &DefaultAPITestService{} + service.update = func(testObject TestObject) (ImplResponse, error) { + assert.FailNow(t, "service should not be called when required field is missing") + return Response(status, nil), nil + } + + controller := NewDefaultAPIController(service) + controller.Update(w, req) + + res := w.Result() + assert.Equal(t, status, res.StatusCode) + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, "\"field 'n' is required.\"\n", string(data)) +} + +func TestUpdateRejectsUnknownField(t *testing.T) { + const ( + payload = `{"name":"","n":0,"unknown":true}` + status = http.StatusBadRequest + ) + + req := httptest.NewRequest(http.MethodPut, "/config", strings.NewReader(payload)) + w := httptest.NewRecorder() + + service := &DefaultAPITestService{} + service.update = func(testObject TestObject) (ImplResponse, error) { + assert.FailNow(t, "service should not be called when an unknown field is present") + return Response(status, nil), nil + } + + controller := NewDefaultAPIController(service) + controller.Update(w, req) + + res := w.Result() + assert.Equal(t, status, res.StatusCode) + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Contains(t, string(data), "unknown field") +} + +func TestTestObjectUnmarshalJSONZeroValue(t *testing.T) { + var obj TestObject + err := json.Unmarshal([]byte(`{"name":"","n":0}`), &obj) + require.NoError(t, err) + assert.Equal(t, "", obj.Name) + assert.Equal(t, int32(0), obj.N) +} diff --git a/samples/server/others/go-server/required-zero-value/go/error.go b/samples/server/others/go-server/required-zero-value/go/error.go new file mode 100644 index 000000000000..8da894447d09 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/error.go @@ -0,0 +1,74 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +import ( + "errors" + "fmt" + "net/http" +) + +var ( + // ErrTypeAssertionError is thrown when type an interface does not match the asserted type + ErrTypeAssertionError = errors.New("unable to assert type") +) + +// ParsingError indicates that an error has occurred when parsing request parameters +type ParsingError struct { + Param string + Err error +} + +func (e *ParsingError) Unwrap() error { + return e.Err +} + +func (e *ParsingError) Error() string { + if e.Param == "" { + return e.Err.Error() + } + + return e.Param + ": " + e.Err.Error() +} + +// RequiredError indicates that an error has occurred when parsing request parameters +type RequiredError struct { + Field string +} + +func (e *RequiredError) Error() string { + return fmt.Sprintf("field '%s' is required.", e.Field) +} + +// ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if +// you would like errors to be handled differently from the DefaultErrorHandler +type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error, result *ImplResponse) + +// DefaultErrorHandler defines the default logic on how to handle errors from the controller. Any errors from parsing +// request params will return a StatusBadRequest. Otherwise, the error code originating from the servicer will be used. +func DefaultErrorHandler(w http.ResponseWriter, _ *http.Request, err error, result *ImplResponse) { + var parsingErr *ParsingError + if ok := errors.As(err, &parsingErr); ok { + // Handle parsing errors + _ = EncodeJSONResponse(err.Error(), func(i int) *int { return &i }(http.StatusBadRequest), map[string][]string{}, w) + return + } + + var requiredErr *RequiredError + if ok := errors.As(err, &requiredErr); ok { + // Handle missing required errors + _ = EncodeJSONResponse(err.Error(), func(i int) *int { return &i }(http.StatusUnprocessableEntity), map[string][]string{}, w) + return + } + + // Handle all other errors + _ = EncodeJSONResponse(err.Error(), &result.Code, result.Headers, w) +} diff --git a/samples/server/others/go-server/required-zero-value/go/helpers.go b/samples/server/others/go-server/required-zero-value/go/helpers.go new file mode 100644 index 000000000000..2b2255ec14ac --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/helpers.go @@ -0,0 +1,383 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +import ( + "encoding/json" + "errors" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +const errMsgRequiredMissing = "required parameter is missing" +const errMsgMinValueConstraint = "provided parameter is not respecting minimum value constraint" +const errMsgMaxValueConstraint = "provided parameter is not respecting maximum value constraint" + +// Response return a ImplResponse struct filled +func Response(code int, body interface{}) ImplResponse { + return ImplResponse { + Code: code, + Headers: nil, + Body: body, + } +} + +// ResponseWithHeaders return a ImplResponse struct filled, including headers +func ResponseWithHeaders(code int, headers map[string][]string, body interface{}) ImplResponse { + return ImplResponse { + Code: code, + Headers: headers, + Body: body, + } +} + +// IsZeroValue checks if the val is the zero-ed value. +func IsZeroValue(val interface{}) bool { + return val == nil || reflect.DeepEqual(val, reflect.Zero(reflect.TypeOf(val)).Interface()) +} + +// AssertRecurseInterfaceRequired recursively checks each struct in a slice against the callback. +// This method traverse nested slices in a preorder fashion. +func AssertRecurseInterfaceRequired[T any](obj interface{}, callback func(T) error) error { + return AssertRecurseValueRequired(reflect.ValueOf(obj), callback) +} + +// AssertRecurseValueRequired checks each struct in the nested slice against the callback. +// This method traverse nested slices in a preorder fashion. ErrTypeAssertionError is thrown if +// the underlying struct does not match type T. +func AssertRecurseValueRequired[T any](value reflect.Value, callback func(T) error) error { + switch value.Kind() { + // If it is a struct we check using callback + case reflect.Struct: + obj, ok := value.Interface().(T) + if !ok { + return ErrTypeAssertionError + } + + if err := callback(obj); err != nil { + return err + } + + // If it is a slice we continue recursion + case reflect.Slice: + for i := 0; i < value.Len(); i++ { + if err := AssertRecurseValueRequired(value.Index(i), callback); err != nil { + return err + } + } + } + return nil +} + +// EncodeJSONResponse uses the json encoder to write an interface to the http response with an optional status code +func EncodeJSONResponse(i interface{}, status *int, headers map[string][]string, w http.ResponseWriter) error { + wHeader := w.Header() + for key, values := range headers { + for _, value := range values { + wHeader.Add(key, value) + } + } + + f, ok := i.(*os.File) + if ok { + data, err := io.ReadAll(f) + if err != nil { + return err + } + wHeader.Set("Content-Type", http.DetectContentType(data)) + wHeader.Set("Content-Disposition", "attachment; filename="+f.Name()) + if status != nil { + w.WriteHeader(*status) + } else { + w.WriteHeader(http.StatusOK) + } + _, err = w.Write(data) + return err + } + wHeader.Set("Content-Type", "application/json; charset=UTF-8") + + if status != nil { + w.WriteHeader(*status) + } else { + w.WriteHeader(http.StatusOK) + } + + if i != nil { + return json.NewEncoder(w).Encode(i) + } + + return nil +} + +// ReadFormFileToTempFile reads file data from a request form and writes it to a temporary file +func ReadFormFileToTempFile(r *http.Request, key string) (*os.File, error) { + _, fileHeader, err := r.FormFile(key) + if err != nil { + return nil, err + } + + return readFileHeaderToTempFile(fileHeader) +} + +// ReadFormFilesToTempFiles reads files array data from a request form and writes it to a temporary files +func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) { + if err := r.ParseMultipartForm(32 << 20); err != nil { + return nil, err + } + + files := make([]*os.File, 0, len(r.MultipartForm.File[key])) + + for _, fileHeader := range r.MultipartForm.File[key] { + file, err := readFileHeaderToTempFile(fileHeader) + if err != nil { + for _, f := range files { + f.Close() + os.Remove(f.Name()) + } + return nil, err + } + + files = append(files, file) + } + + return files, nil +} + +// readFileHeaderToTempFile reads multipart.FileHeader and writes it to a temporary file +func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error) { + formFile, err := fileHeader.Open() + if err != nil { + return nil, err + } + + defer formFile.Close() + + // Use .* as suffix, because the asterisk is a placeholder for the random value, + // and the period allows consumers of this file to remove the suffix to obtain the original file name + file, err := os.CreateTemp("", fileHeader.Filename+".*") + if err != nil { + return nil, err + } + + _, err = io.Copy(file, formFile) + if err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + // Rewind so the caller can read from the beginning. + // The caller is responsible for closing the returned file. + if _, err := file.Seek(0, io.SeekStart); err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + return file, nil +} + +func parseTimes(param string) ([]time.Time, error) { + splits := strings.Split(param, ",") + times := make([]time.Time, 0, len(splits)) + for _, v := range splits { + t, err := parseTime(v) + if err != nil { + return nil, err + } + times = append(times, t) + } + return times, nil +} + +// parseTime will parses a string parameter into a time.Time using the RFC3339 format +func parseTime(param string) (time.Time, error) { + if param == "" { + return time.Time{}, nil + } + return time.Parse(time.RFC3339, param) +} + +type Number interface { + ~int32 | ~int64 | ~float32 | ~float64 +} + +type ParseString[T Number | string | bool] func(v string) (T, error) + +// parseFloat64 parses a string parameter to an float64. +func parseFloat64(param string) (float64, error) { + if param == "" { + return 0, nil + } + + return strconv.ParseFloat(param, 64) +} + +// parseFloat32 parses a string parameter to an float32. +func parseFloat32(param string) (float32, error) { + if param == "" { + return 0, nil + } + + v, err := strconv.ParseFloat(param, 32) + return float32(v), err +} + +// parseInt64 parses a string parameter to an int64. +func parseInt64(param string) (int64, error) { + if param == "" { + return 0, nil + } + + return strconv.ParseInt(param, 10, 64) +} + +// parseInt32 parses a string parameter to an int32. +func parseInt32(param string) (int32, error) { + if param == "" { + return 0, nil + } + + val, err := strconv.ParseInt(param, 10, 32) + return int32(val), err +} + +// parseBool parses a string parameter to an bool. +func parseBool(param string) (bool, error) { + if param == "" { + return false, nil + } + + return strconv.ParseBool(param) +} + +type Operation[T Number | string | bool] func(actual string) (T, bool, error) + +func WithRequire[T Number | string | bool](parse ParseString[T]) Operation[T] { + var empty T + return func(actual string) (T, bool, error) { + if actual == "" { + return empty, false, errors.New(errMsgRequiredMissing) + } + + v, err := parse(actual) + return v, false, err + } +} + +func WithDefaultOrParse[T Number | string | bool](def T, parse ParseString[T]) Operation[T] { + return func(actual string) (T, bool, error) { + if actual == "" { + return def, true, nil + } + + v, err := parse(actual) + return v, false, err + } +} + +func WithParse[T Number | string | bool](parse ParseString[T]) Operation[T] { + return func(actual string) (T, bool, error) { + v, err := parse(actual) + return v, false, err + } +} + +type Constraint[T Number | string | bool] func(actual T) error + +func WithMinimum[T Number](expected T) Constraint[T] { + return func(actual T) error { + if actual < expected { + return errors.New(errMsgMinValueConstraint) + } + + return nil + } +} + +func WithMaximum[T Number](expected T) Constraint[T] { + return func(actual T) error { + if actual > expected { + return errors.New(errMsgMaxValueConstraint) + } + + return nil + } +} + +// parseNumericParameter parses a numeric parameter to its respective type. +func parseNumericParameter[T Number](param string, fn Operation[T], checks ...Constraint[T]) (T, error) { + v, ok, err := fn(param) + if err != nil { + return 0, err + } + + if !ok { + for _, check := range checks { + if err := check(v); err != nil { + return 0, err + } + } + } + + return v, nil +} + +// parseBoolParameter parses a string parameter to a bool +func parseBoolParameter(param string, fn Operation[bool]) (bool, error) { + v, _, err := fn(param) + return v, err +} + +// parseNumericArrayParameter parses a string parameter containing array of values to its respective type. +func parseNumericArrayParameter[T Number](param, delim string, required bool, fn Operation[T], checks ...Constraint[T]) ([]T, error) { + if param == "" { + if required { + return nil, errors.New(errMsgRequiredMissing) + } + + return nil, nil + } + + str := strings.Split(param, delim) + values := make([]T, len(str)) + + for i, s := range str { + v, ok, err := fn(s) + if err != nil { + return nil, err + } + + if !ok { + for _, check := range checks { + if err := check(v); err != nil { + return nil, err + } + } + } + + values[i] = v + } + + return values, nil +} + +// parseQuery parses query parameters and returns an error if any malformed value pairs are encountered. +func parseQuery(rawQuery string) (url.Values, error) { + return url.ParseQuery(rawQuery) +} diff --git a/samples/server/others/go-server/required-zero-value/go/impl.go b/samples/server/others/go-server/required-zero-value/go/impl.go new file mode 100644 index 000000000000..8e20b7bbfac3 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/impl.go @@ -0,0 +1,18 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +// ImplResponse defines an implementation response with error code and the associated body +type ImplResponse struct { + Code int + Headers map[string][]string + Body interface{} +} diff --git a/samples/server/others/go-server/required-zero-value/go/logger.go b/samples/server/others/go-server/required-zero-value/go/logger.go new file mode 100644 index 000000000000..ba25841eed4d --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/logger.go @@ -0,0 +1,33 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +import ( + "net/http" + "log" + "time" +) + +func Logger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s %s %s %s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} diff --git a/samples/server/others/go-server/required-zero-value/go/model_base_object.go b/samples/server/others/go-server/required-zero-value/go/model_base_object.go new file mode 100644 index 000000000000..4e04e8134ff5 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/model_base_object.go @@ -0,0 +1,95 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + + +import ( + "encoding/json" + "fmt" +) + + + +type BaseObject struct { + + Kind string `json:"kind,omitempty"` + + Name string `json:"name"` +} +// UnmarshalJSON validates required property keys then unmarshals into BaseObject +func (o *BaseObject) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "name", + } + + requiredNullableProperties := map[string]bool{ + "name": false, + } + + allowedJsonKeys := map[string]struct{}{ + "kind": {}, + "name": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded BaseObject + + if value, exists := allProperties["kind"]; exists { + if err = json.Unmarshal(value, &decoded.Kind); err != nil { + return err + } + } + if value, exists := allProperties["name"]; exists { + if err = json.Unmarshal(value, &decoded.Name); err != nil { + return err + } + } + + *o = decoded + + return nil +} + +// AssertBaseObjectRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertBaseObjectRequired(obj BaseObject) error { + return nil +} + +// AssertBaseObjectConstraints checks if the values respects the defined constraints +func AssertBaseObjectConstraints(obj BaseObject) error { + return nil +} diff --git a/samples/server/others/go-server/required-zero-value/go/model_test_object.go b/samples/server/others/go-server/required-zero-value/go/model_test_object.go new file mode 100644 index 000000000000..c877b583bbb3 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/model_test_object.go @@ -0,0 +1,110 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + + +import ( + "errors" + "encoding/json" + "fmt" +) + + + +type TestObject struct { + BaseObject + + N int32 `json:"n"` +} +// UnmarshalJSON validates required property keys then unmarshals into TestObject +func (o *TestObject) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "name", + "n", + } + + requiredNullableProperties := map[string]bool{ + "name": false, + "n": false, + } + + allowedJsonKeys := map[string]struct{}{ + "kind": {}, + "name": {}, + "n": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded TestObject + + if value, exists := allProperties["kind"]; exists { + if err = json.Unmarshal(value, &decoded.Kind); err != nil { + return err + } + } + if value, exists := allProperties["name"]; exists { + if err = json.Unmarshal(value, &decoded.Name); err != nil { + return err + } + } + if value, exists := allProperties["n"]; exists { + if err = json.Unmarshal(value, &decoded.N); err != nil { + return err + } + } + + *o = decoded + + return nil +} + +// AssertTestObjectRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertTestObjectRequired(obj TestObject) error { + if err := AssertBaseObjectRequired(obj.BaseObject); err != nil { + return err + } + + return nil +} + +// AssertTestObjectConstraints checks if the values respects the defined constraints +func AssertTestObjectConstraints(obj TestObject) error { + if obj.N < 0 { + return &ParsingError{Param: "N", Err: errors.New(errMsgMinValueConstraint)} + } + return nil +} diff --git a/samples/server/others/go-server/required-zero-value/go/routers.go b/samples/server/others/go-server/required-zero-value/go/routers.go new file mode 100644 index 000000000000..d1abf66de5ce --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/routers.go @@ -0,0 +1,52 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package petstoreserver + +import ( + "net/http" + "github.com/gorilla/mux" +) + +// A Route defines the parameters for an api endpoint +type Route struct { + Name string + Method string + Pattern string + HandlerFunc http.HandlerFunc +} + +// Routes is a map of defined api endpoints +type Routes map[string]Route + +// Router defines the required methods for retrieving api routes +type Router interface { + Routes() Routes + OrderedRoutes() []Route +} + +// NewRouter creates a new router for any number of api routers +func NewRouter(routers ...Router) *mux.Router { + router := mux.NewRouter().StrictSlash(true) + for _, api := range routers { + for _, route := range api.OrderedRoutes() { + var handler http.Handler = route.HandlerFunc + handler = Logger(handler, route.Name) + + router. + Methods(route.Method). + Path(route.Pattern). + Name(route.Name). + Handler(handler) + } + } + + return router +} diff --git a/samples/server/others/go-server/required-zero-value/main.go b/samples/server/others/go-server/required-zero-value/main.go new file mode 100644 index 000000000000..5735780f50be --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/main.go @@ -0,0 +1,29 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * Test - OpenAPI 3.0 + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.0 + */ + +package main + +import ( + "log" + "net/http" + + petstoreserver "github.com/GIT_USER_ID/GIT_REPO_ID/go" +) + +func main() { + log.Printf("Server started") + + DefaultAPIService := petstoreserver.NewDefaultAPIService() + DefaultAPIController := petstoreserver.NewDefaultAPIController(DefaultAPIService) + + router := petstoreserver.NewRouter(DefaultAPIController) + + log.Fatal(http.ListenAndServe(":8080", router)) +} diff --git a/samples/server/petstore/go-api-server/go/api_fake.go b/samples/server/petstore/go-api-server/go/api_fake.go index 59afbae82e31..bcc180c9253f 100644 --- a/samples/server/petstore/go-api-server/go/api_fake.go +++ b/samples/server/petstore/go-api-server/go/api_fake.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" ) @@ -78,6 +79,11 @@ func (c *FakeAPIController) FakePostTest(w http.ResponseWriter, r *http.Request) d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&bodyParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/petstore/go-api-server/go/api_pet.go b/samples/server/petstore/go-api-server/go/api_pet.go index 6951604ddc85..2b9ce5b07a4b 100644 --- a/samples/server/petstore/go-api-server/go/api_pet.go +++ b/samples/server/petstore/go-api-server/go/api_pet.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" "time" @@ -238,6 +239,11 @@ func (c *PetAPIController) UpdatePet(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&petParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -265,6 +271,11 @@ func (c *PetAPIController) AddPet(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&petParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -654,6 +665,7 @@ func (c *PetAPIController) UploadFile(w http.ResponseWriter, r *http.Request) { } fileParam = param + defer param.Close() } @@ -694,6 +706,11 @@ func (c *PetAPIController) UploadFileArrayOfFiles(w http.ResponseWriter, r *http } filesParam = param + defer func() { + for _, f := range param { + f.Close() + } + }() } diff --git a/samples/server/petstore/go-api-server/go/api_store.go b/samples/server/petstore/go-api-server/go/api_store.go index e4577e2c77db..7c60ca45ed01 100644 --- a/samples/server/petstore/go-api-server/go/api_store.go +++ b/samples/server/petstore/go-api-server/go/api_store.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" @@ -128,6 +129,11 @@ func (c *StoreAPIController) PlaceOrder(w http.ResponseWriter, r *http.Request) d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&orderParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/petstore/go-api-server/go/api_user.go b/samples/server/petstore/go-api-server/go/api_user.go index a6fd15edc67d..070cc13b374e 100644 --- a/samples/server/petstore/go-api-server/go/api_user.go +++ b/samples/server/petstore/go-api-server/go/api_user.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" @@ -176,6 +177,11 @@ func (c *UserAPIController) CreateUserNullable(w http.ResponseWriter, r *http.Re d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userNullableParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -203,6 +209,11 @@ func (c *UserAPIController) CreateUser(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -230,6 +241,11 @@ func (c *UserAPIController) CreateUsersWithArrayInput(w http.ResponseWriter, r * d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -255,6 +271,11 @@ func (c *UserAPIController) CreateUsersWithListInput(w http.ResponseWriter, r *h d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -365,6 +386,11 @@ func (c *UserAPIController) UpdateUser(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/petstore/go-api-server/go/error.go b/samples/server/petstore/go-api-server/go/error.go index ba23f89b8385..9f4f1744dae4 100644 --- a/samples/server/petstore/go-api-server/go/error.go +++ b/samples/server/petstore/go-api-server/go/error.go @@ -45,7 +45,7 @@ type RequiredError struct { } func (e *RequiredError) Error() string { - return fmt.Sprintf("required field '%s' is zero value.", e.Field) + return fmt.Sprintf("field '%s' is required.", e.Field) } // ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if diff --git a/samples/server/petstore/go-api-server/go/helpers.go b/samples/server/petstore/go-api-server/go/helpers.go index 6f0e318d8e1f..3ba5a5fbc40e 100644 --- a/samples/server/petstore/go-api-server/go/helpers.go +++ b/samples/server/petstore/go-api-server/go/helpers.go @@ -145,6 +145,10 @@ func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) { for _, fileHeader := range r.MultipartForm.File[key] { file, err := readFileHeaderToTempFile(fileHeader) if err != nil { + for _, f := range files { + f.Close() + os.Remove(f.Name()) + } return nil, err } @@ -170,13 +174,21 @@ func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error return nil, err } - defer file.Close() - _, err = io.Copy(file, formFile) if err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + // Rewind so the caller can read from the beginning. + // The caller is responsible for closing the returned file. + if _, err := file.Seek(0, io.SeekStart); err != nil { + file.Close() + os.Remove(file.Name()) return nil, err } - + return file, nil } diff --git a/samples/server/petstore/go-api-server/go/model_an_object.go b/samples/server/petstore/go-api-server/go/model_an_object.go index b5b8e6c5276f..288bc16be8c1 100644 --- a/samples/server/petstore/go-api-server/go/model_an_object.go +++ b/samples/server/petstore/go-api-server/go/model_an_object.go @@ -22,7 +22,8 @@ type AnObject struct { Pet []Pet `json:"Pet,omitempty"` } -// AssertAnObjectRequired checks if the required fields are not zero-ed +// AssertAnObjectRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertAnObjectRequired(obj AnObject) error { if err := AssertTagRequired(obj.Tag); err != nil { return err diff --git a/samples/server/petstore/go-api-server/go/model_api_response.go b/samples/server/petstore/go-api-server/go/model_api_response.go index c6bbed4687a3..42ff7e66467f 100644 --- a/samples/server/petstore/go-api-server/go/model_api_response.go +++ b/samples/server/petstore/go-api-server/go/model_api_response.go @@ -23,7 +23,8 @@ type ApiResponse struct { Message string `json:"message,omitempty"` } -// AssertApiResponseRequired checks if the required fields are not zero-ed +// AssertApiResponseRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertApiResponseRequired(obj ApiResponse) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_category.go b/samples/server/petstore/go-api-server/go/model_category.go index a8c835c0da4d..b8e36a206e55 100644 --- a/samples/server/petstore/go-api-server/go/model_category.go +++ b/samples/server/petstore/go-api-server/go/model_category.go @@ -21,7 +21,8 @@ type Category struct { Name string `json:"name,omitempty" validate:"regexp=^[a-zA-Z0-9]+[a-zA-Z0-9\\\\.\\\\-_]*[a-zA-Z0-9]+$"` } -// AssertCategoryRequired checks if the required fields are not zero-ed +// AssertCategoryRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertCategoryRequired(obj Category) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_colour.go b/samples/server/petstore/go-api-server/go/model_colour.go index a77312b65379..580c819fa5e3 100644 --- a/samples/server/petstore/go-api-server/go/model_colour.go +++ b/samples/server/petstore/go-api-server/go/model_colour.go @@ -55,8 +55,8 @@ func NewColourFromValue(v string) (Colour, error) { } - -// AssertColourRequired checks if the required fields are not zero-ed +// AssertColourRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertColourRequired(obj Colour) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_gender.go b/samples/server/petstore/go-api-server/go/model_gender.go index 410b61fd08d8..4b3eca5ef82d 100644 --- a/samples/server/petstore/go-api-server/go/model_gender.go +++ b/samples/server/petstore/go-api-server/go/model_gender.go @@ -55,8 +55,8 @@ func NewGenderFromValue(v string) (Gender, error) { } - -// AssertGenderRequired checks if the required fields are not zero-ed +// AssertGenderRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertGenderRequired(obj Gender) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_order.go b/samples/server/petstore/go-api-server/go/model_order.go index 50e934993586..f6ecaa053ebe 100644 --- a/samples/server/petstore/go-api-server/go/model_order.go +++ b/samples/server/petstore/go-api-server/go/model_order.go @@ -13,11 +13,14 @@ package petstoreserver import ( "time" + "encoding/json" + "fmt" ) type Order struct { + SpecialInfo PetId int64 `json:"petId,omitempty"` @@ -34,18 +37,114 @@ type Order struct { Comment *string `json:"comment"` } +// UnmarshalJSON validates required property keys then unmarshals into Order +func (o *Order) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "comment", + } -// AssertOrderRequired checks if the required fields are not zero-ed -func AssertOrderRequired(obj Order) error { - elements := map[string]interface{}{ - "comment": obj.Comment, + requiredNullableProperties := map[string]bool{ + "comment": true, + } + + allowedJsonKeys := map[string]struct{}{ + "petId": {}, + "quantity": {}, + "shipDate": {}, + "promotion": {}, + "type": {}, + "id": {}, + "status": {}, + "complete": {}, + "comment": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded Order + + if value, exists := allProperties["petId"]; exists { + if err = json.Unmarshal(value, &decoded.PetId); err != nil { + return err + } + } + if value, exists := allProperties["quantity"]; exists { + if err = json.Unmarshal(value, &decoded.Quantity); err != nil { + return err + } + } + if value, exists := allProperties["shipDate"]; exists { + if err = json.Unmarshal(value, &decoded.ShipDate); err != nil { + return err + } + } + if value, exists := allProperties["promotion"]; exists { + if err = json.Unmarshal(value, &decoded.Promotion); err != nil { + return err + } + } + if value, exists := allProperties["type"]; exists { + if err = json.Unmarshal(value, &decoded.Type); err != nil { + return err + } + } + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["status"]; exists { + if err = json.Unmarshal(value, &decoded.Status); err != nil { + return err + } + } + if value, exists := allProperties["complete"]; exists { + if err = json.Unmarshal(value, &decoded.Complete); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["comment"]; exists { + if err = json.Unmarshal(value, &decoded.Comment); err != nil { + return err } } + *o = decoded + + return nil +} + +// AssertOrderRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertOrderRequired(obj Order) error { + if err := AssertSpecialInfoRequired(obj.SpecialInfo); err != nil { + return err + } + return nil } diff --git a/samples/server/petstore/go-api-server/go/model_order_info.go b/samples/server/petstore/go-api-server/go/model_order_info.go index 21f5d24946c2..4e1000ff7634 100644 --- a/samples/server/petstore/go-api-server/go/model_order_info.go +++ b/samples/server/petstore/go-api-server/go/model_order_info.go @@ -27,7 +27,8 @@ type OrderInfo struct { ShipDate time.Time `json:"shipDate,omitempty"` } -// AssertOrderInfoRequired checks if the required fields are not zero-ed +// AssertOrderInfoRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertOrderInfoRequired(obj OrderInfo) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_pet.go b/samples/server/petstore/go-api-server/go/model_pet.go index 38a5af1d3935..5f8e0e5b4277 100644 --- a/samples/server/petstore/go-api-server/go/model_pet.go +++ b/samples/server/petstore/go-api-server/go/model_pet.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // Pet - A pet for sale in the pet store @@ -30,19 +35,94 @@ type Pet struct { // Deprecated Status string `json:"status,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into Pet +func (o *Pet) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "name", + "photoUrls", + } -// AssertPetRequired checks if the required fields are not zero-ed -func AssertPetRequired(obj Pet) error { - elements := map[string]interface{}{ - "name": obj.Name, - "photoUrls": obj.PhotoUrls, + requiredNullableProperties := map[string]bool{ + "name": false, + "photoUrls": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "category": {}, + "name": {}, + "photoUrls": {}, + "tags": {}, + "status": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) } } + var decoded Pet + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["category"]; exists { + if err = json.Unmarshal(value, &decoded.Category); err != nil { + return err + } + } + if value, exists := allProperties["name"]; exists { + if err = json.Unmarshal(value, &decoded.Name); err != nil { + return err + } + } + if value, exists := allProperties["photoUrls"]; exists { + if err = json.Unmarshal(value, &decoded.PhotoUrls); err != nil { + return err + } + } + if value, exists := allProperties["tags"]; exists { + if err = json.Unmarshal(value, &decoded.Tags); err != nil { + return err + } + } + if value, exists := allProperties["status"]; exists { + if err = json.Unmarshal(value, &decoded.Status); err != nil { + return err + } + } + + *o = decoded + + return nil +} + +// AssertPetRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertPetRequired(obj Pet) error { if obj.Category != nil { if err := AssertCategoryRequired(*obj.Category); err != nil { return err diff --git a/samples/server/petstore/go-api-server/go/model_special_info.go b/samples/server/petstore/go-api-server/go/model_special_info.go index 7be94aa62e33..30a74abd6c30 100644 --- a/samples/server/petstore/go-api-server/go/model_special_info.go +++ b/samples/server/petstore/go-api-server/go/model_special_info.go @@ -21,7 +21,8 @@ type SpecialInfo struct { Type string `json:"type,omitempty"` } -// AssertSpecialInfoRequired checks if the required fields are not zero-ed +// AssertSpecialInfoRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertSpecialInfoRequired(obj SpecialInfo) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_species.go b/samples/server/petstore/go-api-server/go/model_species.go index f236e29c48a1..2969a078e626 100644 --- a/samples/server/petstore/go-api-server/go/model_species.go +++ b/samples/server/petstore/go-api-server/go/model_species.go @@ -64,8 +64,8 @@ func NewSpeciesFromValue(v string) (Species, error) { } - -// AssertSpeciesRequired checks if the required fields are not zero-ed +// AssertSpeciesRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertSpeciesRequired(obj Species) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_tag.go b/samples/server/petstore/go-api-server/go/model_tag.go index 88fb735f34cc..3c83700976ca 100644 --- a/samples/server/petstore/go-api-server/go/model_tag.go +++ b/samples/server/petstore/go-api-server/go/model_tag.go @@ -21,7 +21,8 @@ type Tag struct { Name string `json:"name,omitempty"` } -// AssertTagRequired checks if the required fields are not zero-ed +// AssertTagRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertTagRequired(obj Tag) error { return nil } diff --git a/samples/server/petstore/go-api-server/go/model_user.go b/samples/server/petstore/go-api-server/go/model_user.go index 29803083edbf..0ee8cbca0c24 100644 --- a/samples/server/petstore/go-api-server/go/model_user.go +++ b/samples/server/petstore/go-api-server/go/model_user.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // User - A User who is purchasing from the pet store @@ -39,18 +44,116 @@ type User struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into User +func (o *User) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "deepSliceModel", + } -// AssertUserRequired checks if the required fields are not zero-ed -func AssertUserRequired(obj User) error { - elements := map[string]interface{}{ - "deepSliceModel": obj.DeepSliceModel, + requiredNullableProperties := map[string]bool{ + "deepSliceModel": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "username": {}, + "firstName": {}, + "lastName": {}, + "email": {}, + "password": {}, + "phone": {}, + "userStatus": {}, + "deepSliceModel": {}, + "deepSliceMap": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded User + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["username"]; exists { + if err = json.Unmarshal(value, &decoded.Username); err != nil { + return err + } + } + if value, exists := allProperties["firstName"]; exists { + if err = json.Unmarshal(value, &decoded.FirstName); err != nil { + return err + } + } + if value, exists := allProperties["lastName"]; exists { + if err = json.Unmarshal(value, &decoded.LastName); err != nil { + return err + } + } + if value, exists := allProperties["email"]; exists { + if err = json.Unmarshal(value, &decoded.Email); err != nil { + return err + } + } + if value, exists := allProperties["password"]; exists { + if err = json.Unmarshal(value, &decoded.Password); err != nil { + return err + } + } + if value, exists := allProperties["phone"]; exists { + if err = json.Unmarshal(value, &decoded.Phone); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["userStatus"]; exists { + if err = json.Unmarshal(value, &decoded.UserStatus); err != nil { + return err } } + if value, exists := allProperties["deepSliceModel"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceModel); err != nil { + return err + } + } + if value, exists := allProperties["deepSliceMap"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceMap); err != nil { + return err + } + } + + *o = decoded + + return nil +} +// AssertUserRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertUserRequired(obj User) error { if obj.DeepSliceModel != nil { if err := AssertRecurseInterfaceRequired(*obj.DeepSliceModel, AssertTagRequired); err != nil { return err diff --git a/samples/server/petstore/go-api-server/go/model_user_nullable.go b/samples/server/petstore/go-api-server/go/model_user_nullable.go index 492c204d5dc6..8155f14d1ff2 100644 --- a/samples/server/petstore/go-api-server/go/model_user_nullable.go +++ b/samples/server/petstore/go-api-server/go/model_user_nullable.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // UserNullable - A User who is purchasing from the pet store @@ -39,18 +44,116 @@ type UserNullable struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into UserNullable +func (o *UserNullable) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "deepSliceModel", + } -// AssertUserNullableRequired checks if the required fields are not zero-ed -func AssertUserNullableRequired(obj UserNullable) error { - elements := map[string]interface{}{ - "deepSliceModel": obj.DeepSliceModel, + requiredNullableProperties := map[string]bool{ + "deepSliceModel": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "username": {}, + "firstName": {}, + "lastName": {}, + "email": {}, + "password": {}, + "phone": {}, + "userStatus": {}, + "deepSliceModel": {}, + "deepSliceMap": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded UserNullable + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["username"]; exists { + if err = json.Unmarshal(value, &decoded.Username); err != nil { + return err + } + } + if value, exists := allProperties["firstName"]; exists { + if err = json.Unmarshal(value, &decoded.FirstName); err != nil { + return err + } + } + if value, exists := allProperties["lastName"]; exists { + if err = json.Unmarshal(value, &decoded.LastName); err != nil { + return err + } + } + if value, exists := allProperties["email"]; exists { + if err = json.Unmarshal(value, &decoded.Email); err != nil { + return err + } + } + if value, exists := allProperties["password"]; exists { + if err = json.Unmarshal(value, &decoded.Password); err != nil { + return err + } + } + if value, exists := allProperties["phone"]; exists { + if err = json.Unmarshal(value, &decoded.Phone); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["userStatus"]; exists { + if err = json.Unmarshal(value, &decoded.UserStatus); err != nil { + return err } } + if value, exists := allProperties["deepSliceModel"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceModel); err != nil { + return err + } + } + if value, exists := allProperties["deepSliceMap"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceMap); err != nil { + return err + } + } + + *o = decoded + + return nil +} +// AssertUserNullableRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertUserNullableRequired(obj UserNullable) error { if obj.DeepSliceModel != nil { if err := AssertRecurseInterfaceRequired(*obj.DeepSliceModel, AssertTagRequired); err != nil { return err diff --git a/samples/server/petstore/go-api-server/samples_tests/model_pet_test.go b/samples/server/petstore/go-api-server/samples_tests/model_pet_test.go index 5835268b81de..349148fcd5c4 100644 --- a/samples/server/petstore/go-api-server/samples_tests/model_pet_test.go +++ b/samples/server/petstore/go-api-server/samples_tests/model_pet_test.go @@ -14,9 +14,16 @@ func Test_Model_Pet(t *testing.T) { lines := utils.ReadLines(filepath) expected := "\tId int64 `json:\"id,omitempty\"`" - - if lines[18] != expected { - t.Errorf("Expected '%s', but got '%s'", expected, lines[18]) + + found := false + for _, line := range lines { + if line == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected line '%s' not found in %s", expected, filepath) } }) } diff --git a/samples/server/petstore/go-chi-server/go/api_fake.go b/samples/server/petstore/go-chi-server/go/api_fake.go index 59afbae82e31..bcc180c9253f 100644 --- a/samples/server/petstore/go-chi-server/go/api_fake.go +++ b/samples/server/petstore/go-chi-server/go/api_fake.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" ) @@ -78,6 +79,11 @@ func (c *FakeAPIController) FakePostTest(w http.ResponseWriter, r *http.Request) d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&bodyParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/petstore/go-chi-server/go/api_pet.go b/samples/server/petstore/go-chi-server/go/api_pet.go index 16ed2edfcfd9..1f8f745475d2 100644 --- a/samples/server/petstore/go-chi-server/go/api_pet.go +++ b/samples/server/petstore/go-chi-server/go/api_pet.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" "time" @@ -238,6 +239,11 @@ func (c *PetAPIController) UpdatePet(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&petParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -265,6 +271,11 @@ func (c *PetAPIController) AddPet(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&petParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -647,6 +658,7 @@ func (c *PetAPIController) UploadFile(w http.ResponseWriter, r *http.Request) { } fileParam = param + defer param.Close() } @@ -686,6 +698,11 @@ func (c *PetAPIController) UploadFileArrayOfFiles(w http.ResponseWriter, r *http } filesParam = param + defer func() { + for _, f := range param { + f.Close() + } + }() } diff --git a/samples/server/petstore/go-chi-server/go/api_store.go b/samples/server/petstore/go-chi-server/go/api_store.go index f7144203bf64..19084447a7ea 100644 --- a/samples/server/petstore/go-chi-server/go/api_store.go +++ b/samples/server/petstore/go-chi-server/go/api_store.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" @@ -128,6 +129,11 @@ func (c *StoreAPIController) PlaceOrder(w http.ResponseWriter, r *http.Request) d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&orderParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/petstore/go-chi-server/go/api_user.go b/samples/server/petstore/go-chi-server/go/api_user.go index 577d069a2630..8bfeba217da0 100644 --- a/samples/server/petstore/go-chi-server/go/api_user.go +++ b/samples/server/petstore/go-chi-server/go/api_user.go @@ -12,6 +12,7 @@ package petstoreserver import ( "encoding/json" + "errors" "net/http" "strings" @@ -176,6 +177,11 @@ func (c *UserAPIController) CreateUserNullable(w http.ResponseWriter, r *http.Re d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userNullableParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -203,6 +209,11 @@ func (c *UserAPIController) CreateUser(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -230,6 +241,11 @@ func (c *UserAPIController) CreateUsersWithArrayInput(w http.ResponseWriter, r * d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -255,6 +271,11 @@ func (c *UserAPIController) CreateUsersWithListInput(w http.ResponseWriter, r *h d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } @@ -363,6 +384,11 @@ func (c *UserAPIController) UpdateUser(w http.ResponseWriter, r *http.Request) { d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&userParam); err != nil { + var requiredErr *RequiredError + if errors.As(err, &requiredErr) { + c.errorHandler(w, r, err, nil) + return + } c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } diff --git a/samples/server/petstore/go-chi-server/go/error.go b/samples/server/petstore/go-chi-server/go/error.go index ba23f89b8385..9f4f1744dae4 100644 --- a/samples/server/petstore/go-chi-server/go/error.go +++ b/samples/server/petstore/go-chi-server/go/error.go @@ -45,7 +45,7 @@ type RequiredError struct { } func (e *RequiredError) Error() string { - return fmt.Sprintf("required field '%s' is zero value.", e.Field) + return fmt.Sprintf("field '%s' is required.", e.Field) } // ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if diff --git a/samples/server/petstore/go-chi-server/go/helpers.go b/samples/server/petstore/go-chi-server/go/helpers.go index 6f0e318d8e1f..3ba5a5fbc40e 100644 --- a/samples/server/petstore/go-chi-server/go/helpers.go +++ b/samples/server/petstore/go-chi-server/go/helpers.go @@ -145,6 +145,10 @@ func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) { for _, fileHeader := range r.MultipartForm.File[key] { file, err := readFileHeaderToTempFile(fileHeader) if err != nil { + for _, f := range files { + f.Close() + os.Remove(f.Name()) + } return nil, err } @@ -170,13 +174,21 @@ func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error return nil, err } - defer file.Close() - _, err = io.Copy(file, formFile) if err != nil { + file.Close() + os.Remove(file.Name()) + return nil, err + } + + // Rewind so the caller can read from the beginning. + // The caller is responsible for closing the returned file. + if _, err := file.Seek(0, io.SeekStart); err != nil { + file.Close() + os.Remove(file.Name()) return nil, err } - + return file, nil } diff --git a/samples/server/petstore/go-chi-server/go/model_an_object.go b/samples/server/petstore/go-chi-server/go/model_an_object.go index b5b8e6c5276f..288bc16be8c1 100644 --- a/samples/server/petstore/go-chi-server/go/model_an_object.go +++ b/samples/server/petstore/go-chi-server/go/model_an_object.go @@ -22,7 +22,8 @@ type AnObject struct { Pet []Pet `json:"Pet,omitempty"` } -// AssertAnObjectRequired checks if the required fields are not zero-ed +// AssertAnObjectRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertAnObjectRequired(obj AnObject) error { if err := AssertTagRequired(obj.Tag); err != nil { return err diff --git a/samples/server/petstore/go-chi-server/go/model_api_response.go b/samples/server/petstore/go-chi-server/go/model_api_response.go index c6bbed4687a3..42ff7e66467f 100644 --- a/samples/server/petstore/go-chi-server/go/model_api_response.go +++ b/samples/server/petstore/go-chi-server/go/model_api_response.go @@ -23,7 +23,8 @@ type ApiResponse struct { Message string `json:"message,omitempty"` } -// AssertApiResponseRequired checks if the required fields are not zero-ed +// AssertApiResponseRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertApiResponseRequired(obj ApiResponse) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_category.go b/samples/server/petstore/go-chi-server/go/model_category.go index a8c835c0da4d..b8e36a206e55 100644 --- a/samples/server/petstore/go-chi-server/go/model_category.go +++ b/samples/server/petstore/go-chi-server/go/model_category.go @@ -21,7 +21,8 @@ type Category struct { Name string `json:"name,omitempty" validate:"regexp=^[a-zA-Z0-9]+[a-zA-Z0-9\\\\.\\\\-_]*[a-zA-Z0-9]+$"` } -// AssertCategoryRequired checks if the required fields are not zero-ed +// AssertCategoryRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertCategoryRequired(obj Category) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_colour.go b/samples/server/petstore/go-chi-server/go/model_colour.go index a77312b65379..580c819fa5e3 100644 --- a/samples/server/petstore/go-chi-server/go/model_colour.go +++ b/samples/server/petstore/go-chi-server/go/model_colour.go @@ -55,8 +55,8 @@ func NewColourFromValue(v string) (Colour, error) { } - -// AssertColourRequired checks if the required fields are not zero-ed +// AssertColourRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertColourRequired(obj Colour) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_gender.go b/samples/server/petstore/go-chi-server/go/model_gender.go index 410b61fd08d8..4b3eca5ef82d 100644 --- a/samples/server/petstore/go-chi-server/go/model_gender.go +++ b/samples/server/petstore/go-chi-server/go/model_gender.go @@ -55,8 +55,8 @@ func NewGenderFromValue(v string) (Gender, error) { } - -// AssertGenderRequired checks if the required fields are not zero-ed +// AssertGenderRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertGenderRequired(obj Gender) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_order.go b/samples/server/petstore/go-chi-server/go/model_order.go index 50e934993586..f6ecaa053ebe 100644 --- a/samples/server/petstore/go-chi-server/go/model_order.go +++ b/samples/server/petstore/go-chi-server/go/model_order.go @@ -13,11 +13,14 @@ package petstoreserver import ( "time" + "encoding/json" + "fmt" ) type Order struct { + SpecialInfo PetId int64 `json:"petId,omitempty"` @@ -34,18 +37,114 @@ type Order struct { Comment *string `json:"comment"` } +// UnmarshalJSON validates required property keys then unmarshals into Order +func (o *Order) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "comment", + } -// AssertOrderRequired checks if the required fields are not zero-ed -func AssertOrderRequired(obj Order) error { - elements := map[string]interface{}{ - "comment": obj.Comment, + requiredNullableProperties := map[string]bool{ + "comment": true, + } + + allowedJsonKeys := map[string]struct{}{ + "petId": {}, + "quantity": {}, + "shipDate": {}, + "promotion": {}, + "type": {}, + "id": {}, + "status": {}, + "complete": {}, + "comment": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded Order + + if value, exists := allProperties["petId"]; exists { + if err = json.Unmarshal(value, &decoded.PetId); err != nil { + return err + } + } + if value, exists := allProperties["quantity"]; exists { + if err = json.Unmarshal(value, &decoded.Quantity); err != nil { + return err + } + } + if value, exists := allProperties["shipDate"]; exists { + if err = json.Unmarshal(value, &decoded.ShipDate); err != nil { + return err + } + } + if value, exists := allProperties["promotion"]; exists { + if err = json.Unmarshal(value, &decoded.Promotion); err != nil { + return err + } + } + if value, exists := allProperties["type"]; exists { + if err = json.Unmarshal(value, &decoded.Type); err != nil { + return err + } + } + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["status"]; exists { + if err = json.Unmarshal(value, &decoded.Status); err != nil { + return err + } + } + if value, exists := allProperties["complete"]; exists { + if err = json.Unmarshal(value, &decoded.Complete); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["comment"]; exists { + if err = json.Unmarshal(value, &decoded.Comment); err != nil { + return err } } + *o = decoded + + return nil +} + +// AssertOrderRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertOrderRequired(obj Order) error { + if err := AssertSpecialInfoRequired(obj.SpecialInfo); err != nil { + return err + } + return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_order_info.go b/samples/server/petstore/go-chi-server/go/model_order_info.go index 21f5d24946c2..4e1000ff7634 100644 --- a/samples/server/petstore/go-chi-server/go/model_order_info.go +++ b/samples/server/petstore/go-chi-server/go/model_order_info.go @@ -27,7 +27,8 @@ type OrderInfo struct { ShipDate time.Time `json:"shipDate,omitempty"` } -// AssertOrderInfoRequired checks if the required fields are not zero-ed +// AssertOrderInfoRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertOrderInfoRequired(obj OrderInfo) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_pet.go b/samples/server/petstore/go-chi-server/go/model_pet.go index 38a5af1d3935..5f8e0e5b4277 100644 --- a/samples/server/petstore/go-chi-server/go/model_pet.go +++ b/samples/server/petstore/go-chi-server/go/model_pet.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // Pet - A pet for sale in the pet store @@ -30,19 +35,94 @@ type Pet struct { // Deprecated Status string `json:"status,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into Pet +func (o *Pet) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "name", + "photoUrls", + } -// AssertPetRequired checks if the required fields are not zero-ed -func AssertPetRequired(obj Pet) error { - elements := map[string]interface{}{ - "name": obj.Name, - "photoUrls": obj.PhotoUrls, + requiredNullableProperties := map[string]bool{ + "name": false, + "photoUrls": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "category": {}, + "name": {}, + "photoUrls": {}, + "tags": {}, + "status": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) } } + var decoded Pet + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["category"]; exists { + if err = json.Unmarshal(value, &decoded.Category); err != nil { + return err + } + } + if value, exists := allProperties["name"]; exists { + if err = json.Unmarshal(value, &decoded.Name); err != nil { + return err + } + } + if value, exists := allProperties["photoUrls"]; exists { + if err = json.Unmarshal(value, &decoded.PhotoUrls); err != nil { + return err + } + } + if value, exists := allProperties["tags"]; exists { + if err = json.Unmarshal(value, &decoded.Tags); err != nil { + return err + } + } + if value, exists := allProperties["status"]; exists { + if err = json.Unmarshal(value, &decoded.Status); err != nil { + return err + } + } + + *o = decoded + + return nil +} + +// AssertPetRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertPetRequired(obj Pet) error { if obj.Category != nil { if err := AssertCategoryRequired(*obj.Category); err != nil { return err diff --git a/samples/server/petstore/go-chi-server/go/model_special_info.go b/samples/server/petstore/go-chi-server/go/model_special_info.go index 7be94aa62e33..30a74abd6c30 100644 --- a/samples/server/petstore/go-chi-server/go/model_special_info.go +++ b/samples/server/petstore/go-chi-server/go/model_special_info.go @@ -21,7 +21,8 @@ type SpecialInfo struct { Type string `json:"type,omitempty"` } -// AssertSpecialInfoRequired checks if the required fields are not zero-ed +// AssertSpecialInfoRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertSpecialInfoRequired(obj SpecialInfo) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_species.go b/samples/server/petstore/go-chi-server/go/model_species.go index f236e29c48a1..2969a078e626 100644 --- a/samples/server/petstore/go-chi-server/go/model_species.go +++ b/samples/server/petstore/go-chi-server/go/model_species.go @@ -64,8 +64,8 @@ func NewSpeciesFromValue(v string) (Species, error) { } - -// AssertSpeciesRequired checks if the required fields are not zero-ed +// AssertSpeciesRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertSpeciesRequired(obj Species) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_tag.go b/samples/server/petstore/go-chi-server/go/model_tag.go index 88fb735f34cc..3c83700976ca 100644 --- a/samples/server/petstore/go-chi-server/go/model_tag.go +++ b/samples/server/petstore/go-chi-server/go/model_tag.go @@ -21,7 +21,8 @@ type Tag struct { Name string `json:"name,omitempty"` } -// AssertTagRequired checks if the required fields are not zero-ed +// AssertTagRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. func AssertTagRequired(obj Tag) error { return nil } diff --git a/samples/server/petstore/go-chi-server/go/model_user.go b/samples/server/petstore/go-chi-server/go/model_user.go index 29803083edbf..0ee8cbca0c24 100644 --- a/samples/server/petstore/go-chi-server/go/model_user.go +++ b/samples/server/petstore/go-chi-server/go/model_user.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // User - A User who is purchasing from the pet store @@ -39,18 +44,116 @@ type User struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into User +func (o *User) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "deepSliceModel", + } -// AssertUserRequired checks if the required fields are not zero-ed -func AssertUserRequired(obj User) error { - elements := map[string]interface{}{ - "deepSliceModel": obj.DeepSliceModel, + requiredNullableProperties := map[string]bool{ + "deepSliceModel": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "username": {}, + "firstName": {}, + "lastName": {}, + "email": {}, + "password": {}, + "phone": {}, + "userStatus": {}, + "deepSliceModel": {}, + "deepSliceMap": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded User + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["username"]; exists { + if err = json.Unmarshal(value, &decoded.Username); err != nil { + return err + } + } + if value, exists := allProperties["firstName"]; exists { + if err = json.Unmarshal(value, &decoded.FirstName); err != nil { + return err + } + } + if value, exists := allProperties["lastName"]; exists { + if err = json.Unmarshal(value, &decoded.LastName); err != nil { + return err + } + } + if value, exists := allProperties["email"]; exists { + if err = json.Unmarshal(value, &decoded.Email); err != nil { + return err + } + } + if value, exists := allProperties["password"]; exists { + if err = json.Unmarshal(value, &decoded.Password); err != nil { + return err + } + } + if value, exists := allProperties["phone"]; exists { + if err = json.Unmarshal(value, &decoded.Phone); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["userStatus"]; exists { + if err = json.Unmarshal(value, &decoded.UserStatus); err != nil { + return err } } + if value, exists := allProperties["deepSliceModel"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceModel); err != nil { + return err + } + } + if value, exists := allProperties["deepSliceMap"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceMap); err != nil { + return err + } + } + + *o = decoded + + return nil +} +// AssertUserRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertUserRequired(obj User) error { if obj.DeepSliceModel != nil { if err := AssertRecurseInterfaceRequired(*obj.DeepSliceModel, AssertTagRequired); err != nil { return err diff --git a/samples/server/petstore/go-chi-server/go/model_user_nullable.go b/samples/server/petstore/go-chi-server/go/model_user_nullable.go index 492c204d5dc6..8155f14d1ff2 100644 --- a/samples/server/petstore/go-chi-server/go/model_user_nullable.go +++ b/samples/server/petstore/go-chi-server/go/model_user_nullable.go @@ -11,6 +11,11 @@ package petstoreserver +import ( + "encoding/json" + "fmt" +) + // UserNullable - A User who is purchasing from the pet store @@ -39,18 +44,116 @@ type UserNullable struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +// UnmarshalJSON validates required property keys then unmarshals into UserNullable +func (o *UserNullable) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against required fields that exist on this struct, + // including fields promoted from embedded allOf parents. + requiredProperties := []string{ + "deepSliceModel", + } -// AssertUserNullableRequired checks if the required fields are not zero-ed -func AssertUserNullableRequired(obj UserNullable) error { - elements := map[string]interface{}{ - "deepSliceModel": obj.DeepSliceModel, + requiredNullableProperties := map[string]bool{ + "deepSliceModel": true, + } + + allowedJsonKeys := map[string]struct{}{ + "id": {}, + "username": {}, + "firstName": {}, + "lastName": {}, + "email": {}, + "password": {}, + "phone": {}, + "userStatus": {}, + "deepSliceModel": {}, + "deepSliceMap": {}, + } + + allProperties := make(map[string]json.RawMessage) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + value, exists := allProperties[requiredProperty] + if !exists { + return &RequiredError{Field: requiredProperty} + } + if string(value) == "null" && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + for key := range allProperties { + if _, exists := allowedJsonKeys[key]; !exists { + return fmt.Errorf("json: unknown field %q", key) + } + } + + var decoded UserNullable + + if value, exists := allProperties["id"]; exists { + if err = json.Unmarshal(value, &decoded.Id); err != nil { + return err + } + } + if value, exists := allProperties["username"]; exists { + if err = json.Unmarshal(value, &decoded.Username); err != nil { + return err + } + } + if value, exists := allProperties["firstName"]; exists { + if err = json.Unmarshal(value, &decoded.FirstName); err != nil { + return err + } + } + if value, exists := allProperties["lastName"]; exists { + if err = json.Unmarshal(value, &decoded.LastName); err != nil { + return err + } + } + if value, exists := allProperties["email"]; exists { + if err = json.Unmarshal(value, &decoded.Email); err != nil { + return err + } + } + if value, exists := allProperties["password"]; exists { + if err = json.Unmarshal(value, &decoded.Password); err != nil { + return err + } + } + if value, exists := allProperties["phone"]; exists { + if err = json.Unmarshal(value, &decoded.Phone); err != nil { + return err + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + if value, exists := allProperties["userStatus"]; exists { + if err = json.Unmarshal(value, &decoded.UserStatus); err != nil { + return err } } + if value, exists := allProperties["deepSliceModel"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceModel); err != nil { + return err + } + } + if value, exists := allProperties["deepSliceMap"]; exists { + if err = json.Unmarshal(value, &decoded.DeepSliceMap); err != nil { + return err + } + } + + *o = decoded + + return nil +} +// AssertUserNullableRequired checks complex required fields (models, arrays, maps) and embedded parents. +// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid. +func AssertUserNullableRequired(obj UserNullable) error { if obj.DeepSliceModel != nil { if err := AssertRecurseInterfaceRequired(*obj.DeepSliceModel, AssertTagRequired); err != nil { return err