diff --git a/.github/workflows/samples-go.yaml b/.github/workflows/samples-go.yaml index edd397a2e8c6..99f844d29d4e 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 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..49bc5f288844 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; @@ -314,6 +315,60 @@ public ModelsMap postProcessModels(ModelsMap objs) { continue; } + if (model.hasRequired) { + Set ownVarBaseNames = model.vars.stream() + .map(v -> v.baseName) + .collect(Collectors.toSet()); + + // Own required fields plus inherited required fields from allOf parents that + // are not flattened into this struct. Inherited keys are presence-checked + // against the raw JSON map, then stripped before strict decode. + List presenceCheckRequiredVars = new ArrayList<>(); + for (CodegenProperty v : model.vars) { + if (v.required && !v.isReadOnly) { + presenceCheckRequiredVars.add(v); + } + } + for (CodegenProperty v : model.requiredVars) { + if (v.required && !v.isReadOnly && !ownVarBaseNames.contains(v.baseName)) { + presenceCheckRequiredVars.add(v); + } + } + model.vendorExtensions.put("presenceCheckRequiredVars", presenceCheckRequiredVars); + boolean hasPresenceCheckRequiredVars = !presenceCheckRequiredVars.isEmpty(); + model.vendorExtensions.put("hasPresenceCheckRequiredVars", hasPresenceCheckRequiredVars); + + if (hasPresenceCheckRequiredVars) { + imports.add(createMapping("import", "bytes")); + imports.add(createMapping("import", "encoding/json")); + } + + List allowedJsonKeys = new ArrayList<>(); + for (CodegenProperty v : model.vars) { + if (!v.isReadOnly) { + allowedJsonKeys.add(v.baseName); + } + } + model.vendorExtensions.put("allowedJsonKeys", allowedJsonKeys); + // Models with embedded parent structs (allOf) have valid JSON keys + // from the parent that the strict decoder already knows about via + // embedding, so we skip the key-filtering step and let the decoder + // reject truly unknown fields. + model.vendorExtensions.put("x-go-has-parent", model.parent != null); + + 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); + } + } + Boolean importErrors = false; for (CodegenProperty param : Iterables.concat(model.vars, model.allVars, model.requiredVars, model.optionalVars)) { 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..f1c3ea597d8e 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" @@ -614,6 +614,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..aed64d54a174 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("required field '%s' is missing.", 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/model.mustache b/modules/openapi-generator/src/main/resources/go-server/model.mustache index 28dd5d3cb61e..e566aadb575b 100644 --- a/modules/openapi-generator/src/main/resources/go-server/model.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/model.mustache @@ -74,20 +74,29 @@ 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}} 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..c76feb20b5b4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/go-server/model_unmarshal_json_presence.mustache @@ -0,0 +1,85 @@ +type _{{classname}} {{classname}} + +// UnmarshalJSON validates required property keys then unmarshals into {{classname}} +func (o *{{classname}}) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + requiredProperties := []string{ +{{#vendorExtensions.presenceCheckRequiredVars}} + "{{baseName}}", +{{/vendorExtensions.presenceCheckRequiredVars}} + } + + requiredNullableProperties := map[string]bool{ +{{#vendorExtensions.presenceCheckRequiredVars}} + "{{baseName}}": {{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}, +{{/vendorExtensions.presenceCheckRequiredVars}} + } + + {{^vendorExtensions.x-go-has-parent}} + allowedJsonKeys := []string{ +{{#vendorExtensions.allowedJsonKeys}} + "{{.}}", +{{/vendorExtensions.allowedJsonKeys}} + } + + {{/vendorExtensions.x-go-has-parent}} + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + var filteredData []byte + {{#vendorExtensions.x-go-has-parent}} + // Parent struct fields are embedded; the strict decoder already knows them. + filteredData, err = json.Marshal(allProperties) + {{/vendorExtensions.x-go-has-parent}} + {{^vendorExtensions.x-go-has-parent}} + {{#isAdditionalPropertiesTrue}} + filteredData, err = json.Marshal(allProperties) + {{/isAdditionalPropertiesTrue}} + {{^isAdditionalPropertiesTrue}} + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value + } + } + + filteredData, err = json.Marshal(filteredProperties) + {{/isAdditionalPropertiesTrue}} + {{/vendorExtensions.x-go-has-parent}} + if err != nil { + return err + } + + var{{classname}} := _{{classname}}{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + {{^isAdditionalPropertiesTrue}} + decoder.DisallowUnknownFields() + {{/isAdditionalPropertiesTrue}} + err = decoder.Decode(&var{{classname}}) + + if err != nil { + return err + } + + *o = {{classname}}(var{{classname}}) + + 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..b9607683a07b 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,82 @@ 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"), + "type _TestObject TestObject"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "requiredProperties := []string{"); + TestUtils.assertFileContains(Paths.get(output + "/go/model_test_object.go"), + "decoder.DisallowUnknownFields()"); + 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 value == nil && !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 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\","); + // Parent structs skip key filtering; they marshal allProperties directly. + TestUtils.assertFileContains(modelPath, "filteredData, err = json.Marshal(allProperties)"); + } + private static CodegenConfigurator createDefaultCodegenConfigurator(File output) { return new CodegenConfigurator() .setGeneratorName("go-server") @@ -149,13 +225,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..5416d845b78c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/go-server/required-zero-value.yaml @@ -0,0 +1,30 @@ +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: + TestObject: + 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..d1c9caf8e218 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, "\"required field 'requireTest' is missing.\"\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..944c5346ca71 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 } 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..e557bf1ceeee 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("required field '%s' is missing.", 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/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..1a8b14577489 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,6 +13,8 @@ package petstoreserver import ( "time" + "bytes" + "encoding/json" ) @@ -34,19 +36,66 @@ type Order struct { Comment *string `json:"comment"` } +type _Order Order + +// UnmarshalJSON validates required property keys then unmarshals into Order +func (o *Order) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + requiredProperties := []string{ + "comment", + "requireTest", + } -// 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{ + "comment": true, + "requireTest": false, + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} } } + var filteredData []byte + // Parent struct fields are embedded; the strict decoder already knows them. + filteredData, err = json.Marshal(allProperties) + if err != nil { + return err + } + + varOrder := _Order{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOrder) + + if err != nil { + return err + } + + *o = Order(varOrder) + + 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 { 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..36af52c2c19b 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 ( + "bytes" + "encoding/json" +) + // Pet - A pet for sale in the pet store @@ -30,19 +35,81 @@ type Pet struct { // Deprecated Status string `json:"status,omitempty"` } +type _Pet Pet -// 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, +// UnmarshalJSON validates required property keys then unmarshals into Pet +func (o *Pet) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + requiredProperties := []string{ + "name", + "photoUrls", + } + + requiredNullableProperties := map[string]bool{ + "name": false, + "photoUrls": true, + } + + allowedJsonKeys := []string{ + "id", + "category", + "name", + "photoUrls", + "tags", + "status", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varPet := _Pet{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varPet) + + if err != nil { + return err + } + + *o = Pet(varPet) + + 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..ea92b0c72cad 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 ( + "bytes" + "encoding/json" +) + // SpecialInfo - An order info for a pets from the pet store @@ -22,18 +27,76 @@ type SpecialInfo struct { Type string `json:"type,omitempty"` } +type _SpecialInfo SpecialInfo -// AssertSpecialInfoRequired checks if the required fields are not zero-ed -func AssertSpecialInfoRequired(obj SpecialInfo) error { - elements := map[string]interface{}{ - "requireTest": obj.RequireTest, +// UnmarshalJSON validates required property keys then unmarshals into SpecialInfo +func (o *SpecialInfo) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + requiredProperties := []string{ + "requireTest", + } + + requiredNullableProperties := map[string]bool{ + "requireTest": false, + } + + allowedJsonKeys := []string{ + "promotion", + "requireTest", + "type", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varSpecialInfo := _SpecialInfo{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varSpecialInfo) + + if err != nil { + return err + } + + *o = SpecialInfo(varSpecialInfo) + + 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..19cdfe2bb7ea 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 ( + "bytes" + "encoding/json" +) + // User - A User who is purchasing from the pet store @@ -39,18 +44,83 @@ type User struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +type _User User + +// UnmarshalJSON validates required property keys then unmarshals into User +func (o *User) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + 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 := []string{ + "id", + "username", + "firstName", + "lastName", + "email", + "password", + "phone", + "userStatus", + "deepSliceModel", + "deepSliceMap", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varUser := _User{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varUser) + + if err != nil { + return err + } + + *o = User(varUser) + + 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..3cd0e9178e72 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("required field '%s' is missing.", 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/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..cdf557dd3320 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("required field '%s' is missing.", 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/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..d0a8330e31d0 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/.openapi-generator/FILES @@ -0,0 +1,14 @@ +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_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..15f776bb83ab --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/README.md @@ -0,0 +1,3 @@ +# Required zero value + +Sample go-server project verifying that required primitive fields accept zero values in JSON request bodies. 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..5416d845b78c --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/api/openapi.yaml @@ -0,0 +1,30 @@ +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: + TestObject: + type: object + properties: + n: + type: integer + minimum: 0 + required: + - n 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..722ea37ca1f4 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go.mod @@ -0,0 +1,14 @@ +module github.com/GIT_USER_ID/GIT_REPO_ID + +go 1.18 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) 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..1270b8dc6601 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api.go @@ -0,0 +1,26 @@ +// 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 +type DefaultAPIRouter interface { + Update(http.ResponseWriter, *http.Request) +} + +// DefaultAPIServicer defines the api actions for the DefaultAPI service +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..958a8e282490 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api_default.go @@ -0,0 +1,102 @@ +// 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"), + "/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"), + "/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 err != nil { + c.errorHandler(w, r, err, &result) + return + } + _ = 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..260c68527402 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api_default_service.go @@ -0,0 +1,31 @@ +// 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" + "errors" + "net/http" +) + +// DefaultAPIService is a service that implements the logic for the DefaultAPIServicer +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) { + 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..ebe62102cc77 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/api_default_test.go @@ -0,0 +1,84 @@ +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 = `{"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, 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 = `{}` + 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, "\"required field 'n' is missing.\"\n", string(data)) +} + +func TestTestObjectUnmarshalJSONZeroValue(t *testing.T) { + var obj TestObject + err := json.Unmarshal([]byte(`{"n":0}`), &obj) + require.NoError(t, err) + 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..cdf557dd3320 --- /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. + +/* + * optional body + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.1 + */ + +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("required field '%s' is missing.", 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..cf8d1f5ae9fe --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/helpers.go @@ -0,0 +1,371 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +/* + * optional body + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.1 + */ + +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 { + 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 + } + + defer file.Close() + + _, err = io.Copy(file, formFile) + if err != nil { + 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..f3c7cddd76f5 --- /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. + +/* + * optional body + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.1 + */ + +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..e8f8a441e9ca --- /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. + +/* + * optional body + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.1 + */ + +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_test_object.go b/samples/server/others/go-server/required-zero-value/go/model_test_object.go new file mode 100644 index 000000000000..37bc23992a21 --- /dev/null +++ b/samples/server/others/go-server/required-zero-value/go/model_test_object.go @@ -0,0 +1,97 @@ +// 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 ( + "bytes" + "encoding/json" + "errors" +) + +type TestObject struct { + N int32 `json:"n"` +} + +type _TestObject TestObject + +// UnmarshalJSON validates required property keys then unmarshals into TestObject +func (o *TestObject) UnmarshalJSON(data []byte) (err error) { + requiredProperties := []string{ + "n", + } + + requiredNullableProperties := map[string]bool{ + "n": false, + } + + allowedJsonKeys := []string{ + "n", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } + } + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value + } + } + + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varTestObject := _TestObject{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varTestObject) + + if err != nil { + return err + } + + *o = TestObject(varTestObject) + + 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 { + 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..8bb178252c77 --- /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. + +/* + * optional body + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 1.0.1 + */ + +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..89c48f4b1c7c 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 } 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..e557bf1ceeee 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("required field '%s' is missing.", 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/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..49d3ce78e5ec 100644 --- a/samples/server/petstore/go-api-server/go/model_order.go +++ b/samples/server/petstore/go-api-server/go/model_order.go @@ -13,6 +13,8 @@ package petstoreserver import ( "time" + "bytes" + "encoding/json" ) @@ -34,18 +36,64 @@ type Order struct { Comment *string `json:"comment"` } +type _Order Order + +// UnmarshalJSON validates required property keys then unmarshals into Order +func (o *Order) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + 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, + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} } } + var filteredData []byte + // Parent struct fields are embedded; the strict decoder already knows them. + filteredData, err = json.Marshal(allProperties) + if err != nil { + return err + } + + varOrder := _Order{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOrder) + + if err != nil { + return err + } + + *o = Order(varOrder) + + 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 { 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..36af52c2c19b 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 ( + "bytes" + "encoding/json" +) + // Pet - A pet for sale in the pet store @@ -30,19 +35,81 @@ type Pet struct { // Deprecated Status string `json:"status,omitempty"` } +type _Pet Pet -// 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, +// UnmarshalJSON validates required property keys then unmarshals into Pet +func (o *Pet) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + requiredProperties := []string{ + "name", + "photoUrls", + } + + requiredNullableProperties := map[string]bool{ + "name": false, + "photoUrls": true, + } + + allowedJsonKeys := []string{ + "id", + "category", + "name", + "photoUrls", + "tags", + "status", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varPet := _Pet{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varPet) + + if err != nil { + return err + } + + *o = Pet(varPet) + + 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..19cdfe2bb7ea 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 ( + "bytes" + "encoding/json" +) + // User - A User who is purchasing from the pet store @@ -39,18 +44,83 @@ type User struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +type _User User + +// UnmarshalJSON validates required property keys then unmarshals into User +func (o *User) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + 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 := []string{ + "id", + "username", + "firstName", + "lastName", + "email", + "password", + "phone", + "userStatus", + "deepSliceModel", + "deepSliceMap", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varUser := _User{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varUser) + + if err != nil { + return err + } + + *o = User(varUser) + + 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..d1fd0c22ffc2 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 ( + "bytes" + "encoding/json" +) + // UserNullable - A User who is purchasing from the pet store @@ -39,18 +44,83 @@ type UserNullable struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +type _UserNullable UserNullable + +// UnmarshalJSON validates required property keys then unmarshals into UserNullable +func (o *UserNullable) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + 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 := []string{ + "id", + "username", + "firstName", + "lastName", + "email", + "password", + "phone", + "userStatus", + "deepSliceModel", + "deepSliceMap", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varUserNullable := _UserNullable{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varUserNullable) + + if err != nil { + return err + } + + *o = UserNullable(varUserNullable) + + 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..f7bc44527d4c 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 } 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..e557bf1ceeee 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("required field '%s' is missing.", 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/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..49d3ce78e5ec 100644 --- a/samples/server/petstore/go-chi-server/go/model_order.go +++ b/samples/server/petstore/go-chi-server/go/model_order.go @@ -13,6 +13,8 @@ package petstoreserver import ( "time" + "bytes" + "encoding/json" ) @@ -34,18 +36,64 @@ type Order struct { Comment *string `json:"comment"` } +type _Order Order + +// UnmarshalJSON validates required property keys then unmarshals into Order +func (o *Order) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + 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, + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} } } + var filteredData []byte + // Parent struct fields are embedded; the strict decoder already knows them. + filteredData, err = json.Marshal(allProperties) + if err != nil { + return err + } + + varOrder := _Order{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOrder) + + if err != nil { + return err + } + + *o = Order(varOrder) + + 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 { 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..36af52c2c19b 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 ( + "bytes" + "encoding/json" +) + // Pet - A pet for sale in the pet store @@ -30,19 +35,81 @@ type Pet struct { // Deprecated Status string `json:"status,omitempty"` } +type _Pet Pet -// 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, +// UnmarshalJSON validates required property keys then unmarshals into Pet +func (o *Pet) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + requiredProperties := []string{ + "name", + "photoUrls", + } + + requiredNullableProperties := map[string]bool{ + "name": false, + "photoUrls": true, + } + + allowedJsonKeys := []string{ + "id", + "category", + "name", + "photoUrls", + "tags", + "status", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varPet := _Pet{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varPet) + + if err != nil { + return err + } + + *o = Pet(varPet) + + 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..19cdfe2bb7ea 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 ( + "bytes" + "encoding/json" +) + // User - A User who is purchasing from the pet store @@ -39,18 +44,83 @@ type User struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +type _User User + +// UnmarshalJSON validates required property keys then unmarshals into User +func (o *User) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + 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 := []string{ + "id", + "username", + "firstName", + "lastName", + "email", + "password", + "phone", + "userStatus", + "deepSliceModel", + "deepSliceMap", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varUser := _User{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varUser) + + if err != nil { + return err + } + + *o = User(varUser) + + 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..d1fd0c22ffc2 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 ( + "bytes" + "encoding/json" +) + // UserNullable - A User who is purchasing from the pet store @@ -39,18 +44,83 @@ type UserNullable struct { // An array 1-deep. DeepSliceMap [][]AnObject `json:"deepSliceMap,omitempty"` } +type _UserNullable UserNullable + +// UnmarshalJSON validates required property keys then unmarshals into UserNullable +func (o *UserNullable) UnmarshalJSON(data []byte) (err error) { + // Presence is checked against own required fields and any inherited required + // fields from allOf parents that are not flattened into this struct. Only keys + // that map to struct fields are passed to the strict decoder. + 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 := []string{ + "id", + "username", + "firstName", + "lastName", + "email", + "password", + "phone", + "userStatus", + "deepSliceModel", + "deepSliceMap", + } + + allProperties := make(map[string]interface{}) + + 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 value == nil && !requiredNullableProperties[requiredProperty] { + return &RequiredError{Field: requiredProperty} + } } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Field: name} + + var filteredData []byte + filteredProperties := make(map[string]interface{}) + for _, allowedKey := range allowedJsonKeys { + if value, exists := allProperties[allowedKey]; exists { + filteredProperties[allowedKey] = value } } + filteredData, err = json.Marshal(filteredProperties) + if err != nil { + return err + } + + varUserNullable := _UserNullable{} + + decoder := json.NewDecoder(bytes.NewReader(filteredData)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varUserNullable) + + if err != nil { + return err + } + + *o = UserNullable(varUserNullable) + + 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