Skip to content

fix: avoid parsing scalar structs twice - #441

Open
otrumb wants to merge 2 commits into
caarlos0:mainfrom
otrumb:fix/scalar-struct-recursion
Open

otrumb wants to merge 2 commits into
caarlos0:mainfrom
otrumb:fix/scalar-struct-recursion

Conversation

@otrumb

@otrumb otrumb commented Sep 23, 2026

Copy link
Copy Markdown

Fixes #440

Problem

With UseFieldNameByDefault, scalar structs such as url.URL were recursively parsed a second time after the tagged value had already been decoded. This let HOST and PATH overwrite URL internals, and made RequiredIfNoDef demand exported implementation fields.

Fix

Avoid recursive parsing when the field type has already been handled by a scalar parser.

RED evidence

  • url.URL became http://evil/usr/bin because HOST and PATH overwrote Host and Path.
  • RequiredIfNoDef demanded internal url.URL fields such as SCHEME, OPAQUE, and HOST.

Verification

  • go test ./... -count=1
  • go test -race -shuffle=on -count=1 ./...
  • go build ./...
  • go vet ./...

Scope

Two files: parser logic and regression tests.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@karaaslanz

Copy link
Copy Markdown

I traced the new scalar gate against current doParseField semantics and I think the type-level check is a little too broad.
Today an initialized nested *struct with no scalar env key is recursively parsed even if its element type also has a FuncMap parser. This head marks the type scalar before field params are considered, so it skips that existing nested path and then returns after processField even when this particular field had no scalar value to parse.
A focused regression would use the same custom struct type both as a scalar field with env:"SCALAR" and as a preinitialized nested pointer with envPrefix:"NESTED_". On current main, NESTED_VALUE is parsed into the nested field. With this head, the nested pointer is classified as scalar solely because its type is registered in FuncMap; because that field has no own scalar key, nothing is set and the later if scalar { return nil } prevents the nested env from being parsed.
I think the scalar-vs-container decision therefore needs to account for whether this particular field actually has scalar input/default to consume, rather than only whether its type can be scalar-parsed. PR #442 touches the same preinitialized-pointer/FuncMap boundary, so it would also be useful to reconcile its regression with whichever fix is chosen instead of leaving two divergent rules.
Source-level review only; I did not run this branch locally.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@pull-request-size pull-request-size Bot added size/L and removed size/M labels Sep 29, 2026
@otrumb

otrumb commented Sep 29, 2026

Copy link
Copy Markdown
Author

The external report was independently reproduced: RED expected nested nested, got ""; GREEN with this fix.

Rule: scalar-capable structs use the parser only when that field has its own key/default; keyless prefixed pointers recurse. Focused/full CGO=0 tests, go vet, and build pass. PR #442's scalar-key preinitialized-pointer behavior remains preserved.

@caarlos0 caarlos0 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Bot review. Requesting changes for one reproduced parsing regression. No local build or full test suite was run.

Comment thread env.go
Comment on lines +387 to +388
scalar := isScalarStruct(refField.Type(), opts.FuncMap) &&
(params.OwnKey != "" || params.HasDefaultValue)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[P2] Preserve prefixed containers when field names are inferred

With UseFieldNameByDefault: true, parseFieldParams generates OwnKey == "NESTED" for an initialized Nested *scalar field tagged only with envPrefix:"NESTED_". If scalar has a registered FuncMap parser, this condition therefore treats the field as a scalar and skips the nested-container path, even when only NESTED_VALUE is supplied.

I ran the same production-API reproduction against the merge base and this head: the base loads NESTED_VALUE successfully, but this head returns nil and leaves the old nested value unchanged. With RequiredIfNoDef: true, it instead reports that NESTED is missing, although the required child value is present.

Preserve container traversal for explicit prefix-only fields whose key comes solely from automatic field naming, honoring PrefixTagName; retain scalar handling for explicit keys/defaults and scalar fields without a container prefix. Extend TestParseCustomScalarStructAsNestedContainer with UseFieldNameByDefault: true and both values of RequiredIfNoDef, supplying only the prefixed child variable and asserting successful parsing of the child.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

With UseFieldNameByDefault, a url.URL field's Host and Path are overwritten from HOST and PATH

3 participants