Repository: oapi-codegen/oapi-codegen (Go)

Issue:
regression in v2.5.0: marshalling optional array property emits null instead of omitting the array
The changes in https://github.com/oapi-codegen/oapi-codegen/pull/1981 causes nil optional array properties to be marshaled as `null` (and thus not valid according to the schema) in some cases due to the changes in `pkg/codegen/templates/additional-properties.tmpl`, `pkg/codegen/templates/union-and-additional-properties.tmpl`, and `pkg/codegen/templates/union.tmpl`.

What seems to be required to trigger this regression is:
- The array property is part of a union or other structure to trigger one of the above templates
- The array property is optional (not in the `required` list)
- The array property has `x-go-type-skip-optional-pointer: true`
  - This is common (at least for me) from prior versions because slices already can store `nil`, so an extra pointer layer is unnecessary

The change in the generated code looks something like this:
```diff
-       if t.Foo != nil {
-               object["foo"], err = json.Marshal(t.Foo)
-               if err != nil {
-                       return nil, fmt.Errorf("error marshaling 'foo': %w", err)
-               }
+       object["foo"], err = json.Marshal(t.Foo)
+       if err != nil {
+               return nil, fmt.Errorf("error marshaling 'foo': %w", err)
        }
```

I consider this a regression because it causes it to emit a schema-invalid JSON document after parsing a valid one.

Thinking through the logic, I find:
- v2.4.0:
  - Skip emitting a property if it is not required and is nil
- v2.5.0:
  - Skip emitting a property if it has a pointer layer and is nil
- What I _think_ it should do:
  - Skip emitting a property if **either**:
  - It has an optional pointer and is nil (implies it is not required)
  - It is not required and is an inherently nillable type (slice, map) and is nil


Discussion: Change in this behavior is causing exactly the same regression here.

To provide more context, I'm seeing a problem related to #2021, where the setting of `SkipOptionalPointer: globalState.options.OutputOptions.PreferSkipOptionalPointer,` for referenced schema types is using the _global_ setting, not that of the reference.

👋 @mgabeler-lee-6rs - I dug more into my issue/regression and have landed what I think is a fix.

#2042

I'd be curious to see if this does indeed fix the problem for you and if it's related.

@magnetikonline While related, I think your issue is different from mine. In my case, many of the properties in question already had an explicit `x-go-type-skip-optional-pointer: true`, since the extra pointer layer was not needed to represent `nil` for a map or slice type.

My problem is that the codegen now _always_ emits those fields, even if nil, instead of obeying their "not required ⇒ don't emit if nil" behavior from before.

I very much do _not_ want to make them all pointers again, as that will be a messy refactor of the consuming code, and make that code a lot more janky.
> My problem is that the codegen now _always_ emits those fields, even if nil, instead of obeying their "not required ⇒ don't emit if nil" behavior from before.

thanks @mgabeler-lee-6rs for the clarification. 👍
Sorry for the delay, and regression, taking a look hopefully today 👀 
@mgabeler-lee-6rs can you provide a minimal OpenAPI spec (snippet) for what this is triggering for?
@jamietanna Here's a failing test for the first of the three variants noted in the issue description: https://github.com/mgabeler-lee-6rs/oapi-codegen/tree/issue-2031-failing-test/internal/test/issues/issue-2031

The other two variants exhibit the same basic behavior, they are just going through other templates where unions (`anyOf`) are involved to force generation of a custom `MarshalJSON` instead of using `additionalProperties` to trigger that. I can expand the failing tests to encompass those if it helps.
Apologies for the delay - taking a look now. Thanks for the failing test + minimal example, that's super helpful 🙇 
I generally stand by the changes we made in https://github.com/oapi-codegen/oapi-codegen/pull/1981, but absolutely see that we should be taking into account _the type we're operating on_ and whether that's a type that can be nullable i.e. whether it's an array/slice - I'm working on the fix now
https://github.com/oapi-codegen/oapi-codegen/pull/2118 will be the fix 👍 



Fix (patch):
diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go
index 1d03fbdd0..c099752d8 100644
--- a/pkg/codegen/schema.go
+++ b/pkg/codegen/schema.go
@@ -128,12 +128,29 @@ func (p Property) GoTypeDef() string {
 	return typeDef
 }
 
+// RequiresNilCheck indicates whether the generated property should have a nil check performed on it before other checks.
+// This should be used in templates when performing `nil` checks, but NOT when i.e. determining if there should be an optional pointer given to the type - in that case, use `HasOptionalPointer`
+func (p Property) RequiresNilCheck() bool {
+	return p.ZeroValueIsNil() || p.HasOptionalPointer()
+}
+
 // HasOptionalPointer indicates whether the generated property has an optional pointer associated with it.
 // This takes into account the `x-go-type-skip-optional-pointer` extension, allowing a parameter definition to control whether the pointer should be skipped.
 func (p Property) HasOptionalPointer() bool {
 	return p.Required == false && p.Schema.SkipOptionalPointer == false //nolint:staticcheck
 }
 
+// ZeroValueIsNil is a helper function to determine if the given Go type used for this property
+// Will return true if the OpenAPI `type` is:
+// - `array`
+func (p Property) ZeroValueIsNil() bool {
+	if p.Schema.OAPISchema == nil {
+		return false
+	}
+
+	return p.Schema.OAPISchema.Type.Is("array")
+}
+
 // EnumDefinition holds type information for enum
 type EnumDefinition struct {
 	// Schema is the scheme of a type which has a list of enum values, eg, the
diff --git a/pkg/codegen/templates/additional-properties.tmpl b/pkg/codegen/templates/additional-properties.tmpl
index 2f450582d..a103216c9 100644
--- a/pkg/codegen/templates/additional-properties.tmpl
+++ b/pkg/codegen/templates/additional-properties.tmpl
@@ -53,12 +53,12 @@ func (a {{.TypeName}}) MarshalJSON() ([]byte, error) {
     var err error
     object := make(map[string]json.RawMessage)
 {{range .Schema.Properties}}
-{{if .HasOptionalPointer}}if a.{{.GoFieldName}} != nil { {{end}}
+{{if .RequiresNilCheck}}if a.{{.GoFieldName}} != nil { {{end}}
     object["{{.JsonFieldName}}"], err = json.Marshal(a.{{.GoFieldName}})
     if err != nil {
         return nil, fmt.Errorf("error marshaling '{{.JsonFieldName}}': %w", err)
     }
-{{if .HasOptionalPointer}} }{{end}}
+{{if .RequiresNilCheck}} }{{end}}
 {{end}}
     for fieldName, field := range a.AdditionalProperties {
 		object[fieldName], err = json.Marshal(field)
diff --git a/pkg/codegen/templates/union-and-additional-properties.tmpl b/pkg/codegen/templates/union-and-additional-properties.tmpl
index 1c48092cc..6ec69f5e8 100644
--- a/pkg/codegen/templates/union-and-additional-properties.tmpl
+++ b/pkg/codegen/templates/union-and-additional-properties.tmpl
@@ -54,12 +54,12 @@ func (a {{.TypeName}}) MarshalJSON() ([]byte, error) {
         }
     }
 {{range .Schema.Properties}}
-{{if .HasOptionalPointer}}if a.{{.GoFieldName}} != nil { {{end}}
+{{if .RequiresNilCheck}}if a.{{.GoFieldName}} != nil { {{end}}
     object["{{.JsonFieldName}}"], err = json.Marshal(a.{{.GoFieldName}})
     if err != nil {
         return nil, fmt.Errorf("error marshaling '{{.JsonFieldName}}': %w", err)
     }
-{{if .HasOptionalPointer}} }{{end}}
+{{if .RequiresNilCheck}} }{{end}}
 {{end}}
     for fieldName, field := range a.AdditionalProperties {
 		object[fieldName], err = json.Marshal(field)
diff --git a/pkg/codegen/templates/union.tmpl b/pkg/codegen/templates/union.tmpl
index c0385f82e..b27a2aca3 100644
--- a/pkg/codegen/templates/union.tmpl
+++ b/pkg/codegen/templates/union.tmpl
@@ -102,12 +102,12 @@
               }
             }
             {{range .Schema.Properties}}
-            {{if .HasOptionalPointer}}if t.{{.GoFieldName}} != nil { {{end}}
+            {{if .RequiresNilCheck}}if t.{{.GoFieldName}} != nil { {{end}}
                 object["{{.JsonFieldName}}"], err = json.Marshal(t.{{.GoFieldName}})
                 if err != nil {
                     return nil, fmt.Errorf("error marshaling '{{.JsonFieldName}}': %w", err)
                 }
-            {{if .HasOptionalPointer}} }{{end}}
+            {{if .RequiresNilCheck}} }{{end}}
             {{end -}}
             b, err = json.Marshal(object)
         {{end -}}