Skip to content

Fix overlay, jsonpath, schema unmarshaling, and converter bugs#359

Merged
erraggy merged 4 commits intomainfrom
fix/overlay-jsonpath-schema-converter-bugs
Mar 31, 2026
Merged

Fix overlay, jsonpath, schema unmarshaling, and converter bugs#359
erraggy merged 4 commits intomainfrom
fix/overlay-jsonpath-schema-converter-bugs

Conversation

@erraggy
Copy link
Copy Markdown
Owner

@erraggy erraggy commented Mar 31, 2026

Summary

Fixes seven open issues spanning jsonpath evaluation, overlay application, schema JSON unmarshaling, parameter conversion, and operation serialization.

Closes #349
Closes #350
Closes #351
Closes #352
Closes #353
Closes #355
Closes #357

Changes

JSONPath (internal/jsonpath) — fixes #350, #351, #352

Schema JSON (parser/schema_json.go) — fixes #353, #355

Operation Security Marshaling (parser/paths_json.go) — fixes #349

  • Operation.MarshalJSON() fast-path was skipping the slow path when Security != nil, so an empty-but-non-nil slice was omitted instead of serialized as []
  • An empty security array has semantic meaning in OAS (explicitly disables document-level security); it must round-trip correctly (Not possible to get empty operation security array in marshaled output #349)
  • Added SetIfSliceNotNil generic helper to jsonhelpers to distinguish nil (omit) from []T{} (serialize as [])

Parameter Conversion (converter/helpers.go) — fixes #357

  • convertOAS2ParameterToOAS3 was only copying Type/Format when building the OAS 3.x schema from an inline-typed parameter; all OAS 2.0 validation keywords (minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength, maxLength, pattern, enum, default, multipleOf, minItems, maxItems, uniqueItems) and nested items are now transferred (converter: array parameter items lost when converting OAS 2.0 → 3.0 #357)
  • Non-csv collectionFormat on items now emits a conversion warning, matching the existing top-level parameter warning

Build (Makefile)

  • Replaced local GOROOT/PATH SDK pinning with export GOTOOLCHAIN = auto — portable across CI and all developer machines without hardcoded ~/sdk/go1.25.8 paths

Test Plan

  • make check passes (lint + format + all tests)
  • jsonpath: TestRemoveArrayIndex_Splices, TestRemoveArrayFilter_Splices, TestRemoveRecursiveDescent, TestRemoveRecursive_ThroughArrays, TestModifyRoot, TestModifyRecursiveDescent
  • overlay: TestApplyRootTarget, TestApplyRecursiveRemove, TestApplyArrayFilterRemove
  • parser: TestSchemaJSONRoundTrip subtests for Items, AdditionalProperties, AdditionalItems, UnevaluatedItems, UnevaluatedProperties (both *Schema and bool forms)
  • parser: TestOperationEmptySecurityMarshal including populated and round-trip subtests
  • converter: TestConvertOAS2ParameterToOAS3_ArrayItems, TestConvertOAS2ParameterToOAS3_ValidationKeywords, TestConvertOAS2ParameterToOAS3_ExclusiveKeywords, TestConvertOAS2ParameterToOAS3_ItemsCollectionFormat

🤖 Generated with Claude Code

erraggy and others added 3 commits March 30, 2026 17:19
- fix(jsonpath): Modify() on root path, Remove() splice for arrays and
  recursive descent, filter segments properly update parent references
- fix(parser): Schema.UnmarshalJSON promotes map[string]any back to
  *Schema for Items/AdditionalProperties and related fields; Operation
  marshals empty Security as [] instead of omitting it
- fix(converter): transfer all OAS 2.0 validation keywords and Items
  when converting parameters to OAS 3.x
- fix(makefile): replace local SDK path pinning with GOTOOLCHAIN=auto
- fix(tests): replace t.Fatal/t.Skip nil guards with require.NotNil and
  explicit return to resolve SA5011 staticcheck issues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fix(jsonpath): add RecursiveSegment case to modifyInParent so $..field
  overlay updates actually apply instead of silently no-oping
- fix(jsonpath): add depth limits to removeFromParentAt/modifyInParentAt
  (matching maxRecursionDepth=500 used by recursiveDescend)
- fix(jsonpath): Modify() root path returns error for non-map documents
  instead of silently discarding fn's return value
- fix(parser): promoteSchemaOrBool returns (any, error) so marshal/unmarshal
  failures surface as parse errors rather than silent type-assertion panics
- fix(converter): warn when param.Items.CollectionFormat is non-csv,
  matching the existing top-level parameter warning
- test(jsonpath): assert array splice correctness for index removal;
  add RecursiveSegment-through-arrays and modifyInParent recursive tests
- test(parser): cover AdditionalItems, UnevaluatedItems, UnevaluatedProperties
  promotion in Schema.UnmarshalJSON
- test(converter): cover ExclusiveMaximum/ExclusiveMinimum transfer and
  items.collectionFormat warning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fix(jsonpath): move depth-limit check before child-segment application
  in removeFromParentAt/modifyInParentAt for consistent truncation
  behaviour matching recursiveDescend
- fix(parser): remove unused type-switch variable in promoteSchemaOrBool
- fix(converter): newConverter() now calls New() to match production
  initialization (IncludeInfo defaults to true)
- test(parser): add bool=true cases for additionalProperties, and bool
  preservation tests for additionalItems/unevaluatedItems/unevaluatedProperties
- test(parser): add populated security marshal and empty-security round-trip
  subtests to TestOperationEmptySecurityMarshal
- test(overlay): add root-target overwrites-existing-key subtest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Adds richer OAS2→OAS3 parameter/schema conversion, fixes JSONPath Modify/Remove to support root and recursive splices, promotes JSON-decoded schema fields to concrete types, changes Operation security marshaling to distinguish nil vs non-nil-empty, modernizes some tests to use testify, and updates Makefile toolchain handling.

Changes

Cohort / File(s) Summary
Build Configuration
Makefile
Replaced conditional GOROOT/PATH pinning with GOTOOLCHAIN=auto for quality targets; added check to QUALITY_TARGETS.
Test Updates
converter/converter_test.go, converter/helpers_test.go, parser/parser_test.go, fixer/fixer_corpus_test.go, internal/jsonpath/jsonpath_test.go, overlay/overlay_test.go, parser/paths_json_test.go, parser/schema_json_test.go
Replaced manual nil checks with require.NotNil(...), added new unit tests for parameter conversion, JSONPath behavior, overlay applier cases, schema JSON round-trips, and security marshaling; ensured tests return after t.Skip.
OAS2→OAS3 Parameter Conversion
converter/helpers.go
When converting non-body OAS2 parameters, construct Schema that copies validation/constraint fields (Default, Enum, min/max, lengths, pattern, items, multipleOf, exclusive flags); set Schema.Items from param.Items and emit issue for non-"csv" collection formats.
JSONPath Mutation Logic
internal/jsonpath/eval.go
Path.Remove now no-ops when no matches and splices array elements instead of nil-padding; removeFromParent returns updated parent and supports recursive-descent removals with depth tracking/warnings; Path.Modify supports root ($) in-place modifications and yields clearer type errors.
Schema JSON Unmarshal Promotion
parser/schema_json.go
After stdlib decode, post-process any-typed schema fields (Items, AdditionalProperties, AdditionalItems, UnevaluatedItems, UnevaluatedProperties) to promote map[string]any/array forms back into *Schema (errors wrapped on failure).
Operation Security Serialization
parser/paths_json.go, parser/internal/jsonhelpers/helpers.go
Operation.MarshalJSON fast-path now requires o.Security == nil; use new SetIfSliceNotNil to include non-nil empty security: [] while omitting nil.
Overlay & JSONPath Tests / Helpers
overlay/overlay_test.go, internal/jsonpath/jsonpath_test.go
Added tests verifying root-target merges, recursive removal, and array-filter removals splice elements (no nil entries).
Misc & Utilities
parser/internal/jsonhelpers/helpers.go
Added generic SetIfSliceNotNil[T any] to emit slices only when non-nil (preserve non-nil empty slices).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main changes: fixes to overlay, jsonpath, schema unmarshaling, and converter bugs across multiple areas.
Description check ✅ Passed Description is well-structured and directly related to the changeset, detailing seven fixes with clear explanations of changes and test coverage.
Linked Issues check ✅ Passed All code changes directly address the seven linked issues: #349 (operation security marshaling), #350 (root path modify), #351 (recursive descent removal), #352 (array splice), #353 (schema unmarshaling), #355 (semantic dedup rewriter), #357 (parameter conversion).
Out of Scope Changes check ✅ Passed All changes are directly within scope: jsonpath fixes, schema unmarshaling, operation marshaling, parameter conversion, test additions, and Makefile toolchain update align with PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 88.57% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/overlay-jsonpath-schema-converter-bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 71.90083% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.81%. Comparing base (1896c26) to head (8c81e37).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
parser/schema_json.go 48.48% 9 Missing and 8 partials ⚠️
internal/jsonpath/eval.go 76.66% 11 Missing and 3 partials ⚠️
parser/internal/jsonhelpers/helpers.go 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #359      +/-   ##
==========================================
+ Coverage   84.77%   84.81%   +0.04%     
==========================================
  Files         194      194              
  Lines       27246    27345      +99     
==========================================
+ Hits        23097    23192      +95     
+ Misses       2829     2820       -9     
- Partials     1320     1333      +13     
Files with missing lines Coverage Δ
converter/helpers.go 80.61% <100.00%> (+2.32%) ⬆️
parser/paths_json.go 87.95% <100.00%> (ø)
parser/internal/jsonhelpers/helpers.go 76.37% <0.00%> (-1.85%) ⬇️
internal/jsonpath/eval.go 75.92% <76.66%> (+3.55%) ⬆️
parser/schema_json.go 82.96% <48.48%> (-11.16%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/jsonpath/eval.go (1)

352-388: ⚠️ Potential issue | 🟠 Major

Clear removed slice entries before reslicing.

The new splice paths shorten the slice, but they still leave removed any values in the backing array (v[:0], filter compaction, and the no-allocation index-removal case). On large docs that keeps removed subtrees live until the whole document is dropped.

Proposed fix
 	case IndexSegment:
 		if arr, ok := parent.([]any); ok {
 			idx := s.Index
 			if idx < 0 {
 				idx = len(arr) + idx
 			}
 			if idx >= 0 && idx < len(arr) {
-				return append(arr[:idx:idx], arr[idx+1:]...)
+				copy(arr[idx:], arr[idx+1:])
+				arr[len(arr)-1] = nil
+				return arr[:len(arr)-1]
 			}
 		}
@@
 	case WildcardSegment:
 		switch v := parent.(type) {
 		case map[string]any:
 			for key := range v {
 				delete(v, key)
 			}
 		case []any:
-			return v[:0]
+			clear(v)
+			return v[:0]
 		}
@@
 	case FilterSegment:
 		switch v := parent.(type) {
 		case map[string]any:
 			for key, val := range v {
 				if evalFilter(val, s.Expr) {
 					delete(v, key)
 				}
 			}
 		case []any:
-			result := v[:0]
-			for _, elem := range v {
-				if !evalFilter(elem, s.Expr) {
-					result = append(result, elem)
-				}
-			}
-			return result
+			n := 0
+			for _, elem := range v {
+				if !evalFilter(elem, s.Expr) {
+					v[n] = elem
+					n++
+				}
+			}
+			clear(v[n:])
+			return v[:n]
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/jsonpath/eval.go` around lines 352 - 388, The slice splice paths
leave removed elements live in the backing array; clear those references to
allow GC. In the IndexSegment case (symbol: IndexSegment, variable: arr, field:
s.Index) set arr[idx] = nil before returning the spliced slice; in the
WildcardSegment case for []any (symbol: WildcardSegment, variable: v) nil out
each element (for i := range v { v[i] = nil }) before returning v[:0]; in the
FilterSegment case for []any (symbol: FilterSegment, variable: v, helper:
evalFilter) compact kept elements into result as you already do but then nil any
trailing slots in the original backing array (for i := len(result); i < len(v);
i++ { v[i] = nil }) before returning result so removed subtrees are not
retained.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@converter/helpers_test.go`:
- Around line 722-747: Add a unit test that verifies convertOAS2ParameterToOAS3
handles an array parameter with nil Items: create a
TestConvertOAS2ParameterToOAS3_ArrayWithoutItems that constructs a
parser.Parameter with Type="array" and Items=nil, calls
c.convertOAS2ParameterToOAS3(param, result, "test"), and asserts the returned
parameter and its Schema are non-nil, Schema.Type == "array", and Schema.Items
is nil; reference newConverter() to obtain the Converter and use
ConversionResult to collect issues.

In `@converter/helpers.go`:
- Around line 70-75: The conversion currently copies boolean flags
param.ExclusiveMaximum/ExclusiveMinimum into
converted.Schema.ExclusiveMaximum/ExclusiveMinimum, which is invalid for OAS
3.1+ where these fields are numeric boundaries; update the conversion logic to
detect the target OAS version (e.g., an existing target/version variable or
function context) and: for OAS <3.1 keep the boolean copy, but for OAS 3.1+ set
converted.Schema.ExclusiveMaximum = param.Maximum (or
converted.Schema.ExclusiveMaximum = param.Maximum if param.ExclusiveMaximum is
true) and converted.Schema.ExclusiveMinimum = param.Minimum when the respective
flags are true, or alternatively emit a conversion issue when you cannot derive
the numeric boundary; reference the param.ExclusiveMaximum,
param.ExclusiveMinimum, param.Maximum, param.Minimum and
converted.Schema.ExclusiveMaximum/ExclusiveMinimum symbols when making the
change.

In `@internal/jsonpath/eval.go`:
- Around line 121-127: The root-path Modify branch currently accepts typed-nil
maps because the type assertion only checks map type, so before calling fn(doc)
ensure doc is a non-nil map: cast doc to m, ok := doc.(map[string]any); if !ok
|| m == nil return fmt.Errorf(...); otherwise call fn with m (or doc) as before;
update the error message to indicate a nil-map was provided; also add a
regression test that passes var doc map[string]any (a typed nil) to Modify to
verify it fails fast.

In `@internal/jsonpath/jsonpath_test.go`:
- Around line 1025-1043: TestModifyRoot currently only asserts the happy path;
add negative cases that call Parse("$") and then p.Modify on documents that are
(a) a non-map value (e.g. a string or int) and (b) a typed-nil map like var m
map[string]any = nil, to verify Modify("$") returns an error (using
require.Error) rather than panicking or silently succeeding; locate Parse and
the p.Modify call inside TestModifyRoot and add assertions that the error is
returned and that the original value is unchanged for these cases.

In `@overlay/overlay_test.go`:
- Around line 1220-1225: mustParseSpec currently constructs a raw map and
therefore bypasses the parser-backed code path that Apply() uses; add a
parser-backed test case so the new $ / $.. behavior is exercised end-to-end.
Update tests to include a fixture that parses spec JSON via the real parser (use
parser.ParseResult produced by the parser, or add a helper named like
mustParseSpecFromParser that calls the parser to return *parser.ParseResult) and
use that result in an Apply() test to cover integration behavior rather than
only the raw map path. Ensure the new test uses the same spec JSON as the
existing cases and asserts the same expectations so regressions on parser-backed
paths are detected.

In `@parser/schema_json_test.go`:
- Around line 287-296: Add two regression tests to cover the edge cases: (1)
ensure items boolean values are preserved as bool when unmarshalling (create a
test similar to the existing one that unmarshals `{"type":"array","items":true}`
into a Schema and asserts s.Items is a bool and true), and (2) ensure
items-as-array (tuple form) unmarshals to a slice of *Schema (unmarshal
`{"type":"array","items":[{...},{...}]}` into Schema and assert s.Items is of
type []*Schema with expected length and contents). Reference the existing test
pattern that uses Schema and s.Items to place and implement these new cases.

In `@parser/schema_json.go`:
- Around line 119-140: promoteSchemaOrBool currently only handles
nil/bool/*Schema and map[string]any but must also convert []any (tuple-style
OAS2 "items") into []*Schema like decodeSchemaOrBool does; change
promoteSchemaOrBool to detect case []any, iterate over elements and for each
element call decodeFromMap (or promote using the same logic used for
map[string]any) to produce a []*Schema slice, returning it or an error if any
element fails, and replace the json.Marshal/json.Unmarshal round-trip for
map[string]any with decodeFromMap to decode into a Schema directly (referencing
promoteSchemaOrBool, decodeSchemaOrBool, decodeFromMap, and Schema to locate the
code to update).

---

Outside diff comments:
In `@internal/jsonpath/eval.go`:
- Around line 352-388: The slice splice paths leave removed elements live in the
backing array; clear those references to allow GC. In the IndexSegment case
(symbol: IndexSegment, variable: arr, field: s.Index) set arr[idx] = nil before
returning the spliced slice; in the WildcardSegment case for []any (symbol:
WildcardSegment, variable: v) nil out each element (for i := range v { v[i] =
nil }) before returning v[:0]; in the FilterSegment case for []any (symbol:
FilterSegment, variable: v, helper: evalFilter) compact kept elements into
result as you already do but then nil any trailing slots in the original backing
array (for i := len(result); i < len(v); i++ { v[i] = nil }) before returning
result so removed subtrees are not retained.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c00a7b36-2c0a-4ee6-89df-60455f210049

📥 Commits

Reviewing files that changed from the base of the PR and between 1896c26 and c1bed75.

📒 Files selected for processing (14)
  • Makefile
  • converter/converter_test.go
  • converter/helpers.go
  • converter/helpers_test.go
  • fixer/fixer_corpus_test.go
  • internal/jsonpath/eval.go
  • internal/jsonpath/jsonpath_test.go
  • overlay/overlay_test.go
  • parser/internal/jsonhelpers/helpers.go
  • parser/parser_test.go
  • parser/paths_json.go
  • parser/paths_json_test.go
  • parser/schema_json.go
  • parser/schema_json_test.go

- jsonpath: guard against typed-nil map[string]any in Modify root path
  (a typed nil passes the type assertion but panics on write)
- parser: handle []any in promoteSchemaOrBool for OAS 2.0 tuple-form
  items arrays (matches existing decodeSchemaOrBool YAML path)
- tests: add TestModifyRoot_InvalidDoc for non-map and typed-nil cases
- tests: add Items bool and tuple-array round-trip cases to schema JSON tests
- tests: add TestConvertOAS2ParameterToOAS3_ArrayWithoutItems for nil Items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@parser/schema_json.go`:
- Around line 138-150: The tuple-array branch currently builds a []*Schema and
silently drops promoted non-*Schema values; change the logic in the `case
[]any:` branch to collect all promoted results (not just `*Schema`) into a
`[]any` (e.g., name it `promotedElems`), appending `promoted` for every element
after `promoteSchemaOrBool` succeeds, and return that slice so boolean or
passthrough elements (like `true`) are preserved; update the function return
type and any callers of this function accordingly so the branch can return
`[]any` instead of `[]*Schema`, while keeping the existing error handling from
`promoteSchemaOrBool`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce16e9c7-8b03-46ff-a7ed-b5aee06fd8f1

📥 Commits

Reviewing files that changed from the base of the PR and between c1bed75 and 8c81e37.

📒 Files selected for processing (5)
  • converter/helpers_test.go
  • internal/jsonpath/eval.go
  • internal/jsonpath/jsonpath_test.go
  • parser/schema_json.go
  • parser/schema_json_test.go

@erraggy erraggy merged commit e92b2ec into main Mar 31, 2026
42 checks passed
@erraggy erraggy deleted the fix/overlay-jsonpath-schema-converter-bugs branch March 31, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment