Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/samples-go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,7 +36,7 @@ jobs:
- run: go version
- name: Run test
working-directory: ${{ matrix.sample }}
run: go test -mod=mod -v
run: go test -mod=mod ./... -v

verify:
name: Verify generated Go files with Go tests
Expand Down
8 changes: 8 additions & 0 deletions bin/configs/go-server-required-zero-value.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: go-server
outputDir: samples/server/others/go-server/required-zero-value
inputSpec: modules/openapi-generator/src/test/resources/3_0/go-server/required-zero-value.yaml
templateDir: modules/openapi-generator/src/main/resources/go-server
additionalProperties:
hideGenerationTimestamp: "true"
packageName: petstoreserver
addResponseHeaders: true
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

import static org.openapitools.codegen.utils.StringUtils.camelize;

Expand Down Expand Up @@ -296,6 +297,86 @@ public void processOpts() {
.doNotOverwrite());
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);

for (ModelsMap modelsMap : objs.values()) {
List<Map<String, String>> imports = modelsMap.getImports();
if (imports == null) {
imports = new ArrayList<>();
modelsMap.setImports(imports);
}
for (ModelMap modelMap : modelsMap.getModels()) {
configureModelRequiredValidation(modelMap.getModel(), imports);
}
}

return objs;
}

/**
* Configures UnmarshalJSON presence checks and AssertRequired helpers for request-body models.
* Runs in {@link #postProcessAllModels} so {@code parentModel} is populated for allOf embedding.
*/
private void configureModelRequiredValidation(CodegenModel model, List<Map<String, String>> imports) {
if (model == null || model.isEnum) {
return;
}

// Parent embedding must be wired regardless of required fields.
// Otherwise allOf child models without required properties silently
// drop their parent from the generated Go struct.
boolean hasEmbeddedParent = model.parentModel != null;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (hasEmbeddedParent) {
model.vendorExtensions.put("x-go-embed-parent-classname", model.parentModel.classname);
}
model.vendorExtensions.put("x-go-has-parent", hasEmbeddedParent);

Map<String, CodegenProperty> decodeVarsByBaseName = new LinkedHashMap<>();
for (CodegenProperty v : model.allVars) {
decodeVarsByBaseName.put(v.baseName, v);
}
for (CodegenProperty v : model.vars) {
decodeVarsByBaseName.put(v.baseName, v);
}
List<CodegenProperty> decodeVars = new ArrayList<>(decodeVarsByBaseName.values());
model.vendorExtensions.put("decodeVars", decodeVars);

List<CodegenProperty> presenceCheckRequiredVars = decodeVars.stream()
.filter(v -> v.required && !v.isReadOnly)
.collect(Collectors.toList());

model.vendorExtensions.put("presenceCheckRequiredVars", presenceCheckRequiredVars);
boolean hasPresenceCheckRequiredVars = !presenceCheckRequiredVars.isEmpty();

model.vendorExtensions.put("hasPresenceCheckRequiredVars", hasPresenceCheckRequiredVars);

if (hasPresenceCheckRequiredVars) {
imports.add(createMapping("import", "encoding/json"));
if (!model.isAdditionalPropertiesTrue) {
imports.add(createMapping("import", "fmt"));
}
}

List<String> allowedJsonKeys = decodeVars.stream()
.map(v -> v.baseName)
.collect(Collectors.toList());
model.vendorExtensions.put("allowedJsonKeys", allowedJsonKeys);

boolean hasRequiredAssertVars = model.vars.stream()
.anyMatch(v -> v.required && !v.isReadOnly && !v.isNullable
&& (v.isModel || v.isArray || v.isMap));
model.vendorExtensions.put("hasRequiredAssertVars", hasRequiredAssertVars);
if (hasRequiredAssertVars) {
List<CodegenProperty> assertRequiredVars = model.vars.stream()
.filter(v -> v.required && !v.isReadOnly && !v.isNullable
&& (v.isModel || v.isArray || v.isMap))
.collect(Collectors.toList());
model.vendorExtensions.put("assertRequiredVars", assertRequiredVars);
}
}

@Override
public ModelsMap postProcessModels(ModelsMap objs) {
// The superclass determines the list of required golang imports. The actual list of imports
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ package {{packageName}}
import (
{{#hasBodyParams}}
"encoding/json"
"errors"
{{/hasBodyParams}}
{{#hasOptionalBodyParams}}
"errors"
"io"
{{/hasOptionalBodyParams}}
"net/http"
Expand Down Expand Up @@ -567,6 +567,16 @@ func (c *{{classname}}Controller) {{operationId}}(w http.ResponseWriter, r *http
}

{{paramName}}Param = param
{{#isArray}}
defer func() {
for _, f := range param {
f.Close()
}
}()
{{/isArray}}
{{^isArray}}
defer param.Close()
{{/isArray}}
}
{{/isFile}}
{{#isLong}}{{#isArray}}
Expand Down Expand Up @@ -614,6 +624,11 @@ func (c *{{classname}}Controller) {{operationId}}(w http.ResponseWriter, r *http
d.DisallowUnknownFields()
{{/isAdditionalPropertiesTrue}}
if err := d.Decode(&{{paramName}}Param); err != nil {{^required}}&& !errors.Is(err, io.EOF) {{/required}}{
var requiredErr *RequiredError
if errors.As(err, &requiredErr) {
c.errorHandler(w, r, err, nil)
return
}
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type RequiredError struct {
}

func (e *RequiredError) Error() string {
return fmt.Sprintf("required field '%s' is zero value.", e.Field)
return fmt.Sprintf("field '%s' is required.", e.Field)
}

// ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) {
for _, fileHeader := range r.MultipartForm.File[key] {
file, err := readFileHeaderToTempFile(fileHeader)
if err != nil {
for _, f := range files {
f.Close()
os.Remove(f.Name())
}
return nil, err
}

Expand All @@ -167,13 +171,21 @@ func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error
return nil, err
}

defer file.Close()

_, err = io.Copy(file, formFile)
if err != nil {
file.Close()
os.Remove(file.Name())
return nil, err
}

// Rewind so the caller can read from the beginning.
// The caller is responsible for closing the returned file.
if _, err := file.Seek(0, io.SeekStart); err != nil {
file.Close()
os.Remove(file.Name())
return nil, err
}

return file, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,48 +57,46 @@ func New{{{classname}}}FromValue(v {{{format}}}{{^format}}{{dataType}}{{/format}
{{/isEnum}}{{^isEnum}}{{#description}}
// {{classname}} - {{{description}}}{{/description}}
type {{classname}} struct {
{{#parent}}
{{^isMap}}
{{^isArray}}
{{{parent}}}
{{/isArray}}
{{/isMap}}
{{#isArray}}
Items {{{parent}}}
{{/isArray}}
{{/parent}}
{{#vendorExtensions.x-go-embed-parent-classname}}
{{{.}}}
{{/vendorExtensions.x-go-embed-parent-classname}}
{{#vars}}{{#description}}
// {{{.}}}{{/description}}
{{#deprecated}}
// Deprecated
{{/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}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Unknown JSON properties remain silently accepted despite the strict decoder. The generated presence unmarshaller filters them out before DisallowUnknownFields, and models without required fields bypass this path entirely; validate keys before filtering or decode the original payload with the strict decoder.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/go-server/model.mustache, line 72:

<comment>Unknown JSON properties remain silently accepted despite the strict decoder. The generated presence unmarshaller filters them out before `DisallowUnknownFields`, and models without required fields bypass this path entirely; validate keys before filtering or decode the original payload with the strict decoder.</comment>

<file context>
@@ -57,48 +57,46 @@ func New{{{classname}}}FromValue(v {{{format}}}{{^format}}{{dataType}}{{/format}
-}{{/isEnum}}
+}
+{{#vendorExtensions.hasPresenceCheckRequiredVars}}
+{{>model_unmarshal_json_presence}}
+{{/vendorExtensions.hasPresenceCheckRequiredVars}}
+{{/isEnum}}
</file context>

{{/vendorExtensions.hasPresenceCheckRequiredVars}}
{{/isEnum}}

// Assert{{classname}}Required checks if the required fields are not zero-ed
// Assert{{classname}}Required checks complex required fields (models, arrays, maps) and embedded parents.
// Primitive required fields are validated for JSON request bodies in UnmarshalJSON so zero values remain valid.
func Assert{{classname}}Required(obj {{classname}}) error {
{{#hasRequired}}
{{#vendorExtensions.hasRequiredAssertVars}}
elements := map[string]interface{}{
{{#requiredVars}}{{^isReadOnly}} "{{baseName}}": obj.{{name}},
{{/isReadOnly}}{{/requiredVars}} }
{{#vendorExtensions.assertRequiredVars}}
"{{baseName}}": obj.{{name}},
{{/vendorExtensions.assertRequiredVars}}
}
for name, el := range elements {
if isZero := IsZeroValue(el); isZero {
return &RequiredError{Field: name}
}
}

{{/vendorExtensions.hasRequiredAssertVars}}
{{/hasRequired}}
{{#parent}}
{{^isMap}}
{{^isArray}}
if err := Assert{{{parent}}}Required(obj.{{{parent}}}); err != nil {
{{#vendorExtensions.x-go-embed-parent-classname}}
if err := Assert{{vendorExtensions.x-go-embed-parent-classname}}Required(obj.{{vendorExtensions.x-go-embed-parent-classname}}); err != nil {
return err
}

{{/isArray}}
{{/isMap}}
{{/parent}}
{{/vendorExtensions.x-go-embed-parent-classname}}
{{#Vars}}
{{^isReadOnly}}
{{#isNullable}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// UnmarshalJSON validates required property keys then unmarshals into {{classname}}
func (o *{{classname}}) UnmarshalJSON(data []byte) (err error) {
// Presence is checked against required fields that exist on this struct,
// including fields promoted from embedded allOf parents.
requiredProperties := []string{
{{#vendorExtensions.presenceCheckRequiredVars}}
"{{baseName}}",
{{/vendorExtensions.presenceCheckRequiredVars}}
}

requiredNullableProperties := map[string]bool{
{{#vendorExtensions.presenceCheckRequiredVars}}
"{{baseName}}": {{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}},
{{/vendorExtensions.presenceCheckRequiredVars}}
}

{{^isAdditionalPropertiesTrue}}
allowedJsonKeys := map[string]struct{}{
{{#vendorExtensions.allowedJsonKeys}}
"{{.}}": {},
{{/vendorExtensions.allowedJsonKeys}}
}

{{/isAdditionalPropertiesTrue}}
allProperties := make(map[string]json.RawMessage)

err = json.Unmarshal(data, &allProperties)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

if err != nil {
return err
}

for _, requiredProperty := range requiredProperties {
value, exists := allProperties[requiredProperty]
if !exists {
return &RequiredError{Field: requiredProperty}
}
if string(value) == "null" && !requiredNullableProperties[requiredProperty] {
return &RequiredError{Field: requiredProperty}
}
}

{{^isAdditionalPropertiesTrue}}
for key := range allProperties {
if _, exists := allowedJsonKeys[key]; !exists {
return fmt.Errorf("json: unknown field %q", key)
}
}

{{/isAdditionalPropertiesTrue}}
var decoded {{classname}}

{{#vendorExtensions.decodeVars}}
if value, exists := allProperties["{{baseName}}"]; exists {
if err = json.Unmarshal(value, &decoded.{{name}}); err != nil {
return err
}
}
{{/vendorExtensions.decodeVars}}

*o = decoded

return nil
}
Loading
Loading