Skip to content

Instantly share code, notes, and snippets.

@dims
Last active April 27, 2026 13:13
Show Gist options
  • Select an option

  • Save dims/ecd681ea7d4748300d3b6203074e7d70 to your computer and use it in GitHub Desktop.

Select an option

Save dims/ecd681ea7d4748300d3b6203074e7d70 to your computer and use it in GitHub Desktop.
kube-openapi PR #590 risk analysis: go-openapi/swag v0.23.0→v0.25.4 behavioral deep-dive

kube-openapi PR #590 — Deep-Dive Risk Analysis

Upgrading go-openapi/swag v0.23.0 → v0.25.4

Prepared: 2026-04-27
PR: kubernetes/kube-openapi#590
Reviewer question: "go-openapi has some reputation of changing semantics without notification by accident. As we use it in our CRD validation there is risk that we break our API (we have forked the go-openapi validator nowadays, so risk is lower than in the past, but worth a check anyway)."


Executive Summary

This PR makes four changes: (1) upgrades go-openapi/swag v0.23.0 → v0.25.4, (2) bumps the go directive from 1.23.0 to 1.24.0, (3) upgrades go.yaml.in/yaml/v3 v3.0.3 → v3.0.4, and (4) fixes pre-existing go vet format-string warnings. The analysis below is based on multi-agent investigation, independent code inspection, and empirical testing.

Bottom line: The PR is safe to merge, but not for the reasons initially stated. The yaml/v3 bump is a pure metadata change. The swag upgrade removes mailru/easyjson and josharian/intern from kube-openapi's build dependency path (they remain in the module graph via transitive edges). One intentional behavioral change in IsFloat64AJSONInteger is present and confirmed — it switches from truncation-based comparison to math.Round-based nearest-integer comparison, changing the epsilon tolerance band symmetrically for both positive and negative values: 3.9999999999 and -3.9999999999 now classify as integers (previously did not), while 1.000000001 no longer does (previously did). The key risk point is already moot for Kubernetes master: Kubernetes currently vendors go-openapi/swag v0.25.4 directly, and the vendored kube-openapi source already imports that version at the CRD validation callsite — the IsFloat64AJSONInteger behavioral change is therefore already live in Kubernetes CRD validation today. PR #590's impact is primarily on kube-openapi's own module metadata, standalone consumers, and the go vet fixes. The go 1.24.0 directive is a real minimum-version floor for non-Kubernetes standalone consumers; Kubernetes master is already on Go 1.26.0 and is unaffected.


1. What Changed: The Full Dependency Picture

1.1 go-openapi/swag: Monolith to Sub-Modules

swag v0.25.4 was a major structural refactoring. What was a single Go package became a monorepo with 11 independent Go modules:

Sub-module Content Used by kube-openapi
swag/conv Type conversion utilities (ConvertInt32, IsFloat64AJSONInteger, etc.) YES — validation
swag/jsonname JSON field name inference from struct tags YES — spec Schema.UnmarshalJSON
swag/jsonutils JSON concat, read/write, ordered maps YES — spec3, validation/spec
swag/mangling Go name mangling (ToGoName, ToFileName, etc.) NO
swag/yamlutils YAML-to-JSON conversion Indirect (via loading)
swag/loading HTTP/file loading Indirect
swag/stringutils String search utilities Test only
swag/typeutils Nil checking, zero-value detection Indirect
swag/netutils Host/port utilities NO
swag/fileutils File utilities NO
swag/cmdutils CLI utilities NO

The root package (github.com/go-openapi/swag) now contains only *_iface.go files plus doc.go and tests — the non-test compatibility symbols are thin shims that forward every deprecated symbol to the appropriate sub-module. Implementations have moved entirely to sub-modules. All callers of swag.ConcatJSON, swag.IsFloat64AJSONInteger, etc. continue to compile without changes.

Verification: Every symbol used by kube-openapi (30 files, 30 import statements) was traced to its new location. All are re-exported correctly in the root shim. No symbol was dropped.

1.2 Removed Dependencies

Dependency Removed from Status in module graph
github.com/mailru/easyjson v0.7.7 go.mod direct requirement; go list -deps ./... build path; h1 hash in go.sum Still present in go list -m all and go.sum go.mod hash, via transitive module graph edges (e.g. go-openapi/jsonpointer still references it)
github.com/josharian/intern v1.0.0 Same as above Same as above

Precision matters here: the correct claim is "removed from kube-openapi's build and package dependency path." Running go list -deps ./... on the PR branch does not include easyjson or intern; no compiled binary or object file from those packages is included. However, go list -m all still lists both modules and go.sum retains their go.mod hashes (not their source h1 hashes). This distinction is important: the supply-chain concern around easyjson's unmaintained state is addressed at the binary level; the module graph entry is a passive artefact of transitive go.mod references.

mailru/easyjson is effectively unmaintained (last real commit 2023, no active maintainer). Its removal from the build path is a genuine supply-chain improvement.

1.3 New Indirect Dependencies

The sub-module split adds 11 new // indirect entries to go.mod. These are all internal to the swag refactoring — they have no transitive dependencies beyond go.yaml.in/yaml/v3 and the Go standard library. No new third-party risk surfaces.

Notable: go-openapi/testify/v2 and go-openapi/testify/enable/yaml/v2 appear in go.sum but are test-only fixtures for the swag sub-modules' own test infrastructure. They are not pulled into kube-openapi's binary.

1.4 go.yaml.in/yaml/v3 v3.0.3 → v3.0.4

Finding: This is a no-op. The entire diff between v3.0.3 and v3.0.4 is one line in go.mod:

-go 1.22
+go 1.16

Zero .go source files changed. This was a compatibility fix to broaden which Go toolchains can build the module. There is no change to the YAML parser, encoder, decoder, or any behavior path. Full risk assessment: none.

Background: go.yaml.in/yaml/v3 is the actively maintained successor to the unmaintained gopkg.in/yaml.v3. It is maintained by the YAML org (including Davanum Srinivas, Stefan Prodan, and others). Kubernetes switched to it intentionally.


2. Code-Level Changes in kube-openapi

2.1 pkg/schemaconv — Format String Fixes

Files changed: openapi.go, proto_models.go
Nature: Go vet correction (SA1006/printf lint)

// Before (all 6 occurrences)
c.reportError(err.Error())

// After
c.reportError("%v", err)

reportError has the signature reportError(format string, args ...interface{}). Passing err.Error() as the format string is a go vet printf violation — if the error string contains %, the output would be garbled. The fix is correct and functionally equivalent for any error string not containing % characters.

Behavioral impact: None in practice. The errors come from getMapElementRelationship and getListElementRelationship which parse x-kubernetes-* extension annotations. These error strings do not contain % characters. For a pathological error message containing %v or %s, the new code correctly passes it verbatim while the old code would misformat it.

Caller impact: reportError appends to c.errorMessages, returned by ToSchema* functions. No downstream code is known to string-match on these specific error messages.

2.2 pkg/validation/validate/result.go — Format String Fix

// Before
strippedErrors = append(strippedErrors, fmt.Errorf(strings.TrimPrefix(e.Error(), "IMPORTANT!")))

// After
strippedErrors = append(strippedErrors, fmt.Errorf("%s", strings.TrimPrefix(e.Error(), "IMPORTANT!")))

Same pattern: go vet fix. The IMPORTANT! prefix is added by the composite validator internally. These strings do not contain % characters in any known code path.


3. Behavioral Changes in go-openapi/swag v0.23.0 → v0.25.4

This is the heart of the reviewer's concern. The analysis covers every function used by kube-openapi.

3.1 WriteJSON / ReadJSON — easyjson Removal (HIGH ATTENTION REQUIRED)

Old architecture (v0.23.0):

func WriteJSON(data interface{}) ([]byte, error) {
    if d, ok := data.(ejMarshaler); ok {  // easyjson fast path
        jw := new(jwriter.Writer)
        d.MarshalEasyJSON(jw)
        return jw.BuildBytes()
    }
    if d, ok := data.(json.Marshaler); ok {
        return d.MarshalJSON()
    }
    return json.Marshal(data)
}

New architecture (v0.25.4):

func WriteJSON(value any) ([]byte, error) {
    if orderedMap, isOrdered := value.(ifaces.Ordered); isOrdered {
        orderedMarshaler := adapters.OrderedMarshalAdapterFor(orderedMap)
        if orderedMarshaler != nil {
            defer orderedMarshaler.Redeem()
            return orderedMarshaler.OrderedMarshal(orderedMap)
        }
    }
    marshaler := adapters.MarshalAdapterFor(value)
    if marshaler != nil {
        defer marshaler.Redeem()
        return marshaler.Marshal(value)
    }
    return json.Marshal(value)
}

The new design uses an adapter pattern with a global registry (adapters.Registry). The default adapter is the stdlib (encoding/json). The easyjson adapter is now opt-in and must be explicitly registered at runtime — it is not registered by default and kube-openapi does not register it.

Critical question: Does kube-openapi have any types that implement ejMarshaler (the easyjson interface)? Answer: No. kube-openapi never ran easyjson code generation. The old fast path was never taken for any kube-openapi type. The effective behavior for all kube-openapi types was always the json.Marshal fallback. The new code reaches the same code path.

JSONMapSlice marshaling — real behavioral changes, but irrelevant to kube-openapi's CRD path:

Empirical testing confirms the following differences between v0.23.0 and v0.25.4:

Scenario v0.23.0 result v0.25.4 result
json.Marshal(JSONMapSlice(nil)) {} null
json.Marshal(JSONMapSlice{}) {} {}
json.Marshal(JSONMapSlice{{Key:"x", Value:JSONMapSlice(nil)}}) {"x":{}} {"x":null}
json.Unmarshal([]byte("null"), &prepopulatedSlice) stale entries preserved slice cleared to nil

The mechanism: v0.23.0 JSONMapSlice.MarshalJSON used jwriter.Writer{Flags: jwriter.NilMapAsEmpty | NilSliceAsEmpty} which serialized nil as {}. v0.25.4's stdlib OrderedMarshal calls typeutils.IsNil(value) and returns "null" for a nil JSONMapSlice.

The unmarshal discrepancy is significant in isolation: the old UnmarshalEasyJSON silently preserved stale entries when receiving null; the new OrderedUnmarshal correctly clears to nil.

Why this does not matter for kube-openapi's CRD validation path: kube-openapi does not directly construct, marshal, or unmarshal JSONMapSlice values in its CRD validation or schema conversion code. JSONMapSlice is an internal type used by BytesToYAMLDoc/YAMLToJSON to preserve key ordering during YAML→JSON conversion; the resulting json.RawMessage is what kube-openapi operates on. A YAML document will never produce a nil JSONMapSlice at the top level (it would parse as an absent or empty mapping). The stale-unmarshal issue is unreachable from kube-openapi's usage patterns.

Ordering preservation: Both versions preserve key order in JSONMapSlice for non-nil inputs. The stdlib adapter's OrderedMarshal iterates via Go 1.23's iter.Seq2 from OrderedItems(). For the same non-nil content, output is functionally equivalent.

Risk: None for kube-openapi's CRD path. Real but contained behavioral change in JSONMapSlice itself — relevant only to direct consumers of that type.

3.2 ConcatJSON — Identical Logic, Minor Cleanup

ConcatJSON is used extensively (17 times in pkg/spec3/, 13 times in pkg/validation/spec/) for merging JSON objects during spec serialization.

v0.23.0:

if len(b) < 3 { // yep empty but also the last one...
    if i == last && a > 0 {
        if err := buf.WriteByte(closing); err != nil {
            log.Println(err)
        }
    }
    continue
}

v0.25.4 (jsonutils/concat.go):

const minLengthIfNotEmpty = 3
if len(b) < minLengthIfNotEmpty {
    if i == last && a > 0 {
        _ = buf.WriteByte(closing) // never returns err != nil
    }
    continue
}

The core merging logic is identical. The new version:

  • Names the constant minLengthIfNotEmpty (documentation improvement)
  • Suppresses the log.Println(err) call because bytes.Buffer.WriteByte never returns an error (the log call was misleading)
  • Suppresses buf.Write error returns for the same reason

One edge-case difference — non-container blobs: When all inputs are non-container bytes (neither {...} objects nor [...] arrays), the old version can return a buffer containing a null byte ("\x00") due to how opening is left at its zero value and written. The new version returns an empty byte slice in the same scenario. This is a bug fix in v0.25.4.

Empirically confirmed:

OLD: ConcatJSON([]byte("hello"), []byte("world"))  → "h\x00"  (garbage + null byte)
NEW: ConcatJSON([]byte("hello"), []byte("world"))  → ""        (empty, correct)

Risk: None for kube-openapi. All kube-openapi call sites pass valid JSON object fragments ({...}). The non-container bug path is unreachable from spec serialization code. The fix is strictly an improvement.

3.3 IsFloat64AJSONInteger — CONFIRMED BEHAVIORAL CHANGE ⚠️

This function is used in two places in kube-openapi's CRD validation:

  1. pkg/validation/validate/values.go:196MultipleOf validator
  2. pkg/validation/validate/type.go:157isFloatInt type coercion check

Old algorithm (v0.23.0):

func IsFloat64AJSONInteger(f float64) bool {
    if math.IsNaN(f) || math.IsInf(f, 0) || f < minJSONFloat || f > maxJSONFloat {
        return false
    }
    fa := math.Abs(f)
    g := float64(uint64(f))   // <-- truncation, undefined behavior for negative numbers
    ga := math.Abs(g)
    diff := math.Abs(f - g)
    switch {
    case f == g:
        return true
    case f == float64(int64(f)) || f == float64(uint64(f)):
        return true
    case f == 0 || g == 0 || diff < math.SmallestNonzeroFloat64:
        return diff < (epsilon * math.SmallestNonzeroFloat64)
    }
    return diff/math.Min(fa+ga, math.MaxFloat64) < epsilon
}

New algorithm (v0.25.4 conv/):

func IsFloat64AJSONInteger(f float64) bool {
    if math.IsNaN(f) || math.IsInf(f, 0) || f < minJSONFloat || f > maxJSONFloat {
        return false
    }
    rounded := math.Round(f)   // <-- rounds to nearest integer
    if f == rounded {
        return true
    }
    if rounded == 0 {
        return false
    }
    diff := math.Abs(f - rounded)
    if diff == 0 {
        return true
    }
    return diff < epsilon*math.Abs(rounded)  // relative tolerance from rounded value
}

Both algorithms use epsilon = 1e-9. The key difference is the reference point for relative error:

  • Old: reference = float64(uint64(f)) (truncation toward zero). Relative tolerance ≈ diff / (2 * |f|)
  • New: reference = math.Round(f) (nearest integer). Relative tolerance ≈ diff / |rounded|

The new algorithm is algorithmically cleaner and fixes a latent bug: float64(uint64(f)) for negative f is implementation-defined behavior in Go. The old code avoided crashing because negative exact integers were caught by the "optimistic case" check (f == float64(int64(f))), but the handling of near-negative-integers was architecturally fragile.

Empirically verified divergences (inputs are the quotient passed to IsFloat64AJSONInteger, not the user's value):

Quotient passed to fn How it arises OLD result NEW result Direction
3.9999999999 (= 4.0 - 1e-10) multipleOf:1, user value 3.9999999999 false (not integer) true (integer) Looser
3.999999999 (= 4.0 - 1e-9) multipleOf:1, user value 3.999999999 false true Looser
1.000000001 (= 1.0 + 1e-9) multipleOf:1, user value 1.0 + 1e-9 true (integer) false (not integer) Stricter
999.9999999 multipleOf:0.001, user value 0.9999999999 → quotient = 1/0.001 * 0.9999999999 false true Looser
4.9999999998 multipleOf:0.5, user value 2.4999999999 → quotient = 1/0.5 * 2.4999999999 false true Looser

Note: The MultipleOf function computes mult = 1/factor * value for factors < 1, so the value passed into IsFloat64AJSONInteger is the quotient, not the user's original value. A user value of 2.4999999999 with multipleOf: 0.5 produces quotient 4.9999999998, not 2.4999999999.

Boundary analysis — symmetric around nearest integer (positive and negative):

The correct characterization is not "slightly more permissive near positive integers." The new algorithm changes the tolerance band for all values within epsilon * |nearest integer| of that integer, including negative values:

Quotient OLD result NEW result
0.999999999 false true
3.9999999999 false true
-3.9999999999 false true
-4.0000000001 false true
1.000000001 true false

The behavior change is symmetric around the nearest integer in both directions. Old algorithm was asymmetric because float64(uint64(f)) truncates toward zero — for 3.9999999999 it produced reference 3.0 (wrong direction). New math.Round uses 4.0 (correct nearest integer). The tightening at 1.000000001 is the flip side: old algorithm's truncation produced reference 1.0 (correct), making the old code happen to be right by accident; new algorithm also uses 1.0, but the relative tolerance formula differs slightly, placing 1.000000001 (= 1.0 + 1e-9, exactly at epsilon) just outside the band.

Impact on CRD multipleOf validation:

MultipleOf computes mult = 1/factor * value (for factors < 1), then calls IsFloat64AJSONInteger(mult). Returning true accepts the value; false produces a "not a multiple of X" error.

Most practical cases are unaffected (integer multiplications produce exact values). The divergences appear only when floating-point arithmetic produces a quotient within epsilon * |nearest integer| of that integer but outside the old truncation-based tolerance. This is a narrow but real range.

Real-world implications:

  1. CRDs with small float multipleOf (e.g., 0.001, 0.0001): Quotients like 999.9999999 (previously rejected) are now accepted. More correct behavior.
  2. CRDs with multipleOf: 1: Values like 3.9999999999 (within floating-point precision of 4.0) are now accepted. This is correct.
  3. Negative-value CRD fields: Same symmetric behavior — -3.9999999999 is now accepted as a multiple of 1. Previously rejected by the old asymmetric algorithm.
  4. Edge case tightening at 1.000000001: Old code accepted this, new code does not. Represents a value exactly at the epsilon boundary — old code was arguably wrong to accept it.

Risk: Low but real. The change is intentional (swag PR #132, August 2025: "improve equality comparison for floats, ~14% faster"). The net direction is toward more correct floating-point handling for both positive and negative boundary values. No existing kube-openapi tests fail. Operators relying on the old algorithm's specific rejection of sub-epsilon boundary values might observe previously-rejected values now accepted — but those rejections were not semantically meaningful.

3.4 DefaultJSONNameProvider / NameProvider — Identical Behavior

DefaultJSONNameProvider is used in pkg/validation/spec/schema.go (lines 574, 619) to get JSON names for struct fields, used to differentiate known fields from extension fields during schema unmarshaling.

The new jsonname sub-module preserves the same mutex-based NameProvider with identical buildnameIndex logic using reflect. The type is now a type alias (type NameProvider = jsonname.NameProvider), not a re-declaration. The DefaultJSONNameProvider is now an exported variable in the root shim pointing to jsonname.DefaultJSONNameProvider.

Subtle change: In v0.23.0, DefaultJSONNameProvider was initialized by NewNameProvider() at package init. In v0.25.4, it is jsonname.DefaultJSONNameProvider, which is also initialized at package init in the sub-module. The value is pre-populated and shared across all callers. Behavior is identical.

Risk: None.

3.5 Type Conversion Functions (ConvertInt32, ConvertUint32, etc.) — Identical

These functions are used in pkg/validation/validate/values.go for range validation:

case integerFormatInt32:
    _, errVal = swag.ConvertInt32(stringRep)
case integerFormatUint32:
    _, errVal = swag.ConvertUint32(stringRep)

In v0.25.4, these delegate to generic functions via the conv sub-module:

func ConvertInt32(str string) (int32, error) { return conv.ConvertInteger[int32](str) }

The conv.ConvertInteger[int32] uses strconv.ParseInt(str, 10, 64) and bounds-checks against the type's min/max. The underlying strconv.ParseInt behavior is identical. Error messages are identical.

Risk: None.

3.6 FormatUint64 / FormatInt64 / FormatFloat64 — Identical

These use strconv.FormatUint, strconv.FormatInt, strconv.FormatFloat in both versions. The wrappers changed from direct calls to generics (conv.FormatInteger, conv.FormatUinteger, conv.FormatFloat) but produce identical output.

Risk: None.

3.7 ToDynamicJSON — Functionally Identical for kube-openapi Types

swag.ToDynamicJSON(data) is called in pkg/validation/validate/schema.go:150 to convert typed values (e.g., strfmt.DateTime) to dynamic JSON before validation.

Old behavior: json.Marshal(data)json.Unmarshal into interface{}.
New behavior: Routes through adapter pattern → json.MarshalReadJSONjson.Unmarshal.

For all types in kube-openapi (none implement ejMarshaler), both paths call json.Marshal and json.Unmarshal. Output is identical.

Risk: None.

3.8 YAMLToJSON / BytesToYAMLDoc — Error Message Changes

These are not called directly by kube-openapi production code (only via loading sub-module). However, the new yamlutils sub-module introduces a sentinel error pattern:

// Old: plain error
return nil, errors.New("only YAML documents that are objects are supported")

// New: error wrapped with ErrYAML sentinel
return nil, fmt.Errorf("only YAML documents that are objects are supported: %w", ErrYAML)

And some errors are double-wrapped using Go 1.20's multi-error syntax:

return nil, fmt.Errorf("unable to decode YAML sequence value: %w: %w", err, ErrYAML)

This changes the behavior of errors.Is(err, yamlutils.ErrYAML) — callers can now use this sentinel for structured error checking. The error message strings still contain the same human-readable text.

Impact on kube-openapi: kube-openapi does not call errors.Is or errors.As on errors from swag's YAML functions in any production code path examined. The error message text (for human display) is unchanged.

Risk: None for kube-openapi. Low for downstream consumers who string-match on specific YAML error messages from swag.

3.9 GoNamePrefixFunc / Name Mangling — Not Used in kube-openapi

kube-openapi does not use any name mangling functions (swag.ToGoName, swag.ToFileName, swag.ToJSONName, swag.Camelize, swag.GoNamePrefixFunc, swag.AddInitialisms). Confirmed by grep with no results. These are used in go-swagger for code generation. No risk to kube-openapi.

3.10 Root Package Import — Init-Time Side Effects

Importing github.com/go-openapi/swag (the root shim package) at v0.25.4 causes Go to compile all 11 sub-modules into the binary, even if kube-openapi only calls a few exported symbols. Confirmed by examining *_iface.go files: each shim file imports its corresponding sub-module.

Init-time side effect audit: Of all 11 sub-modules, only jsonutils/concat.go has a package-level init() function. It initializes a small lookup map (closerAt) mapping {} and []. There is no I/O, no registration of global state that could conflict, no goroutine launch, and no dependency on environment variables. This init is benign and idempotent.

JSON adapter registry: jsonutils has a mutable global adapter registry (adapters.Registry). The default adapter is encoding/json (stdlib). The easyjson adapter is opt-in and requires an explicit adapters.Registry.Register(...) call. kube-openapi does not import jsonutils/adapters, does not call Register, and has no code path that mutates this global. Any future dependency that calls Register at init time would affect all callers globally — a theoretical risk, not a current one.

Risk: None currently. The init side effects are limited to a 2-entry map. The adapter registry mutation risk is theoretical and would require an explicit code change to manifest.


4. Go 1.24.0 Directive Bump

The go directive in go.mod changes from 1.23.0 to 1.24.0. This is required by the swag sub-modules — they use iter.Seq2 (Go 1.23) and other features that mandate go 1.24 in their own go.mod files.

What a go Directive Change Actually Does

The go directive in a module's go.mod affects:

  1. Toolchain minimum for the module itself: Anyone building kube-openapi directly needs Go ≥ 1.24.
  2. Downstream consumer behavior: When Kubernetes vendors kube-openapi, the vendor toolchain only needs to meet the max go directive it encounters. Kubernetes itself will likely declare go 1.24+ by the time this is merged.
  3. Go vet checks enabled: Go 1.24 enables stricter go vet analyzers (notably the printf check that surfaced the format-string fixes in this PR).
  4. New language features: iter.Seq2 (range-over-function) requires go 1.23. The swag sub-modules use it in their JSONMapSlice implementation.

Kubernetes Vendoring Impact

When Kubernetes runs go mod vendor against a kube-openapi that requires go 1.24.0, the vendor directory will contain the kube-openapi source compiled with whatever toolchain Kubernetes itself uses. The go directive in a vendored dependency's go.mod is informational for go mod resolution but does not require the consumer to change their own go directive.

Kubernetes master is already on Go 1.26.0 (as of early 2026), so the 1.24.0 floor is a strict non-issue for the Kubernetes build. The bump matters only for standalone kube-openapi consumers — anyone running go build against kube-openapi directly (not via Kubernetes vendoring) needs Go ≥ 1.24.0 installed.

Risk: None for Kubernetes; Low/administrative for standalone consumers. Kubernetes is on 1.26.0, well above the new floor.


5. Kubernetes Dependency Chain Analysis

5.1 Kubernetes Already Has swag v0.25.4 — Behavioral Change Is Already Live

The most important finding in this entire analysis: The IsFloat64AJSONInteger behavioral change is already active in current Kubernetes master CRD validation. Here is why:

  1. Kubernetes master go.mod lists github.com/go-openapi/swag v0.25.4 // indirect (required by another dependency, selected by MVS).
  2. When Kubernetes runs go mod vendor, Go's flat vendoring puts one copy of go-openapi/swag into vendor/ — the MVS-selected version, which is v0.25.4. The vendor/modules.txt entry reads ## explicit; go 1.24.0.
  3. The vendored kube-openapi source (v0.0.0-20260317180543-43fb72c5454a, March 17, 2026) imports github.com/go-openapi/swag at the CRD validation callsites (pkg/validation/validate/values.go, type.go). Those imports resolve to the single vendored copy — v0.25.4.
  4. Therefore swag.IsFloat64AJSONInteger used by CRD multipleOf validation in Kubernetes today is already the new math.Round-based implementation.

This means PR #590 has zero incremental behavioral risk for Kubernetes. It only updates kube-openapi's own go.mod to declare the swag version it already resolves to in the Kubernetes vendor tree. The IsFloat64AJSONInteger behavior change happened when Kubernetes last bumped its vendor snapshot to include swag v0.25.4 — not when PR #590 merges.

5.2 The Forked Validator

The reviewer's comment references "we have forked the go-openapi validator." This refers to pkg/validation/ in kube-openapi, which is a fork of github.com/go-openapi/validate. The fork was done to allow kube-openapi to evolve independently from the upstream validator and to add Kubernetes-specific validation logic.

The key paths in the forked validator that use swag:

  • pkg/validation/validate/values.goswag.IsFloat64AJSONInteger (MultipleOf), swag.FormatUint64/Int64/Float64, swag.ConvertInt32/Uint32/Int64/Uint64
  • pkg/validation/validate/type.goswag.IsFloat64AJSONInteger (isFloatInt type coercion)
  • pkg/validation/validate/schema.goswag.ToDynamicJSON
  • pkg/validation/spec/schema.goswag.ConcatJSON, swag.DefaultJSONNameProvider.GetJSONNames

Since kube-openapi forked the validator, changes to the upstream go-openapi/validate package no longer automatically affect Kubernetes. The risk window is narrowly limited to the specific swag functions listed above.

5.3 CRD Validation Code Path

The CRD validation flow in Kubernetes:

apiextensions-apiserver → CRD webhook validation
  → k8s.io/kube-openapi/pkg/validation/validate
    → swag.IsFloat64AJSONInteger (for multipleOf checks)
    → swag.ConvertInt32/Uint32 etc. (for range checks)

The multipleOf constraint in CRDs is relatively uncommon (most CRDs use integer types or string patterns). The floating-point precision boundary cases that diverge between old and new algorithm require the input value to fall within epsilon * |rounded| of an integer but outside the old algorithm's tolerance band. This is an extremely narrow range.


6. v0.24.0 Retraction — Versioning History

The swag go.mod includes:

retract v0.24.0 // bad tagging of the main module: superseded by v0.24.1

This explains the version gap from v0.23.0 to v0.25.4: the refactoring was partially tagged as v0.24.0 (wrong tag on the wrong commit in the monorepo structure), then corrected to v0.24.1, and then released as v0.25.x after the sub-module split stabilized. The retraction is for a tagging error, not for a behavioral bug.


7. Test Coverage Assessment

7.1 What Is Covered

Functionality Test location Coverage
MultipleOf validation values_test.go:132-178, 329-371 Tests with integer factors, not floating-point near-boundary
IsValueValidAgainstRange values_test.go:376-415 ConvertInt32/Uint32 overflow paths
ConcatJSON Exercised by all spec marshal/unmarshal tests Comprehensive
keepRelevantErrors result_test.go:183-191 Basic correctness
Schema validation pkg/validation/validate/schema_test.go Broad, including JSON Schema Test Suite (32 fixture files)
JSON Schema Test Suite pkg/validation/validate/fixtures/jsonschema_suite/ Industry-standard suite

Verification performed on PR #590 branch:

go test ./...                                           PASS  (all packages)
go test ./...  -count=1 -timeout 120s                  PASS
cd test/integration && go test ./...                   PASS
go test -race ./pkg/validation/validate/...            PASS
go test -race ./pkg/validation/spec/...                PASS
go test -race ./pkg/spec3/...                          PASS
go test -race ./pkg/schemaconv/...                     PASS
go vet ./pkg/validation/validate/... ./pkg/schemaconv/...  PASS
go vet ./...                                           FAIL (pre-existing failures in
                                                            pkg/internal/third_party/
                                                            go-json-experiment/json only —
                                                            not introduced by this PR)

Race tests pass for all packages with CRD-validation-relevant swag usage.

7.2 Coverage Gaps

  1. No test for multipleOf with floating-point boundary values. The existing MultipleOf tests use whole-number factors (e.g., multipleOf: 5). There is no test that would catch the IsFloat64AJSONInteger divergence for values like 3.9999999999. The swag sub-module's own conv package tests do cover this, but they're not run as part of kube-openapi's test suite.

  2. No test for c.reportError with %-containing error messages in pkg/schemaconv. The openapi_test.go and smd_test.go files test happy paths only. An error from getMapElementRelationship/getListElementRelationship with a %-containing message would expose the difference — but no such message exists in practice.

  3. No test for typeValidator.isFloatInt with float values near integer boundaries. The isFloatInt path (line 157 of type.go) allows a number-typed field to accept integer values. The IsFloat64AJSONInteger change could affect this type coercion in edge cases.


8. Risk Matrix

Change Affected Code Kubernetes CRD Impact Risk Level Mitigations Available
IsFloat64AJSONInteger algorithm multipleOf, isFloatInt Nearest-integer epsilon behavior changed (symmetric, pos+neg); already live in k8s master Low Already active; add boundary tests as follow-up
easyjson removal from WriteJSON/ReadJSON ToDynamicJSON, spec marshaling None — kube-openapi had no easyjson types None
ConcatJSON implementation move All JSON object merging None — output identical None
JSONMapSlice marshaling architecture YAML-to-JSON conversion None — output identical for typical OpenAPI content None
Error sentinel wrapping in yamlutils YAML error handling None — kube-openapi doesn't inspect YAML errors None
go.yaml.in/yaml/v3 v3.0.3→v3.0.4 All YAML parsing None — zero source changes None
Go directive 1.23.0→1.24.0 Build toolchain None for Kubernetes (already on Go 1.26.0); real floor for standalone consumers Low Kubernetes is on 1.26.0; standalone consumers need Go ≥ 1.24
go.mod sub-module split (11 new entries) Vendoring Kubernetes vendor directory gains 11 new module paths Very Low go mod vendor handles this automatically

9. Cross-Checking: Agent vs Direct Analysis Consistency

This analysis used 5 parallel research agents and independent code inspection. Key findings were cross-checked:

Finding Agent finding Independent verification Consistent?
IsFloat64AJSONInteger diverges Agent 2: algorithm refactor, new uses math.Round, confirmed behavioral change; Agent 1 (stalled): commit ebc624612f, PR #132, "improve equality comparison for floats, ~14% faster" Direct code comparison + empirical test program (/tmp/floattest/) confirming specific divergence cases ✅ Fully consistent
yaml v3 bump is no-op Agent 4: only go.mod change, confirmed Checked GitHub diff programmatically ✅ Fully consistent
WriteJSON/ReadJSON safe for kube-openapi Agent 2: no easyjson types in kube-openapi; Agent 3 (stalled): "easyjson adapter is opt-in" Read source of jsonutils/json.go, adapter registry, stdlib adapter; confirmed via grepping for easyjson in kube-openapi ✅ Fully consistent
All symbols re-exported Agent 2: all 30 import files traced Inspected all *_iface.go files in v0.25.4 root ✅ Fully consistent
Tests pass Agent 2: "go build ./... clean, go test ./... all green" go test ./... executed on PR branch: all packages pass ✅ Fully consistent
k8s already has swag v0.25.4 Not covered by agents curl kubernetes/kubernetes go.mod — confirmed Additional finding

10. Recommendations

10.1 For Approving PR #590

The PR is safe to approve. The behavioral change in IsFloat64AJSONInteger is intentional, has been in production via other paths (Kubernetes already has swag v0.25.4 indirectly), and the direction is toward more correct floating-point handling. All existing tests pass.

Suggested response to the reviewer (defensible, precise):

I checked the CRD-relevant swag usage rather than only relying on root-shim compatibility.

The only behavior change I found in a CRD validation path is swag.IsFloat64AJSONInteger, used by multipleOf and float-as-integer type checks. v0.23.0 used truncation-based (float64(uint64(f))) comparison; v0.25.4 uses nearest-integer math.Round with relative epsilon. That changes sub-epsilon boundary behavior symmetrically around integers, including negative values. Examples: 3.9999999999 and -3.9999999999 now classify as integer, while 1.000000001 no longer does.

I checked the other kube-openapi-used swag helpers: Convert*, Format*, DefaultJSONNameProvider, ContainsStringsCI, ToDynamicJSON, and ConcatJSON. I did not find another CRD validation behavior change. JSONMapSlice marshaling changed for nil inputs ({}null), but kube-openapi does not directly use that type in CRD validation.

Importantly, Kubernetes master already vendors swag v0.25.4 (MVS selected it via another dependency), so this specific IsFloat64AJSONInteger behavior is already active in Kubernetes master CRD validation today. Tests pass, including go test ./..., test/integration, and -race for impacted packages.

Low risk. Suggested non-blocking follow-up: add targeted multipleOf floating-point boundary tests to lock the new behavior.

10.2 Do We Need to Work Around the IsFloat64AJSONInteger Change?

No. Here is the per-callsite analysis:

MultipleOf (values.go:196): mult = data/factor is passed to IsFloat64AJSONInteger. The behavioral change has two directions:

  • More permissive — quotients like 3.9999999999, -3.9999999999, 0.999999999 are now accepted (old: rejected). Previously-rejected CRD instance values become valid. Backward-compatible; no existing valid data breaks.
  • More strict1.000000001 is now rejected (old: accepted). The old algorithm tolerated diff / (2 * |f|) < 1e-9, which at f=1.0 means diff < 2e-9. The new requires diff < 1e-9 * |rounded| = 1e-9 (strict <). So 1.0 + 1e-9 was inside the old band, outside the new.

The stricter case requires a user to submit a value where data/factor = 1.0 + 1e-9 exactly — for example, 0.001000000001 with multipleOf: 0.001. This is theoretically reachable, but the ship has already sailed: Kubernetes master has been running swag v0.25.4 at the CRD validation callsite before this PR was proposed. No reports of broken CRD validation have surfaced.

isFloatInt (type.go:157): This checks raw float values (not quotients) to allow a number-typed JSON value like 3.0 to satisfy an integer schema. The change means 3.9999999999 now passes the integer type check (more permissive). The 1.0 + 1e-9 stricter case would require someone to submit that specific float as a literal integer — not a real-world scenario.

Conclusion: No workaround needed. The only action worth taking is locking the new behavior with tests (see below), so a future algorithm change doesn't silently shift semantics again.

10.3 Suggested Follow-Up Work (Non-Blocking)

  1. Add multipleOf floating-point boundary tests. Add cases to values_test.go covering both the permissive and symmetric directions:
// Lock the math.Round-based nearest-integer behavior
{factor: 0.001, data: 0.9999999999, valid: true},   // quotient 999.9999999 rounds to 1000 within epsilon
{factor: 1,     data: 3.9999999999, valid: true},    // quotient rounds to 4 within epsilon
{factor: 1,     data: -3.9999999999, valid: true},   // symmetric for negative values
  1. Add a schemaconv error path test. Add a fixture with an invalid x-kubernetes-list-type value to pkg/schemaconv tests to exercise the c.reportError path that was changed.

  2. Monitor the adapter registry. The new jsonutils.Registry is a mutable global. If any future dependency calls adapters.Registry.Register(...) at init time, it would affect all callers globally. Consider adding a comment or test asserting only the stdlib adapter is registered in kube-openapi's context.

  3. Track swag v0.25.x for sub-module behavioral drift. The sub-module architecture means future versions of individual sub-modules (e.g., swag/conv@v0.26) could be bumped independently. Set up a process to audit sub-module changelogs separately when they diverge.

10.4 For the Kubernetes Upgrade

When Kubernetes next vendors kube-openapi (after PR #590 merges), there is no incremental behavioral change to CRD validation. Kubernetes already vendors swag v0.25.4 at the CRD validation callsite — the IsFloat64AJSONInteger algorithm change is already live. The kube-openapi bump PR is a metadata update: kube-openapi's own go.mod will now correctly declare the swag version it was already resolving to. Standard go test ./... and CRD integration tests are sufficient; no special validation is required.


Appendix A: Files Changed in PR #590

File Change Risk
.github/workflows/ci.yml Pin actions to SHA; drop Go 1.23, add 1.26 None
go.mod swag v0.23.0→v0.25.4; yaml/v3 v3.0.3→v3.0.4; add 11 sub-module entries; go directive 1.23→1.24 Low (see §4)
go.sum Corresponding hash entries None
pkg/schemaconv/openapi.go c.reportError(err.Error())c.reportError("%v", err) None (vet fix)
pkg/schemaconv/proto_models.go 5× same format string fix None (vet fix)
pkg/validation/validate/result.go fmt.Errorf(str)fmt.Errorf("%s", str) None (vet fix)
test/integration/go.mod Mirror of root go.mod changes None
test/integration/go.sum Mirror of root go.sum changes None

Appendix B: Empirical Test Results for IsFloat64AJSONInteger

Executed test program at /tmp/floattest/main.go comparing old and new algorithms:

Note: the argument to IsFloat64AJSONInteger in the MultipleOf path is the computed quotient 1/factor * value, not the user's raw value. The table below shows the quotient actually passed to the function.

Quotient passed to fn          How it arises                         OLD    NEW    Diff?
------------------------------------------------------------------------------------------
3.9999999999 (4.0 - 1e-10)    multipleOf:1,  user value 3.9999999999  false  true   CHANGE
3.999999999  (4.0 - 1e-9)     multipleOf:1,  user value 3.999999999   false  true   CHANGE
1.000000001  (1.0 + 1e-9)     multipleOf:1,  user value 1.0 + 1e-9    true   false  CHANGE
999.9999999  (1000 - 1e-7)    multipleOf:0.001, user value ~1.0        false  true   CHANGE
4.9999999998 (5.0 - 2e-10)    multipleOf:0.5, user value 2.4999999999 false  true   CHANGE
6715.0 (exact integer)        any factor, exact multiple               true   true
-3.0 (exact negative int)     any factor, exact multiple               true   true
3.0 (= 0.3 / 0.1)            multipleOf:0.1, user value 0.3           true   true
314  (= 3.14 / 0.01)          multipleOf:0.01, user value 3.14         true   true

All divergences are at sub-epsilon-of-integer floating-point boundaries. The root cause is the boundary asymmetry in the old algorithm: float64(uint64(f)) truncates toward zero, so a quotient of 3.9999999999 gets compared against truncated value 3.0 (distance ≈ 1.0, far outside epsilon) instead of the nearest integer 4.0 (distance ≈ 1e-10, inside epsilon). The new math.Round-based algorithm is symmetric and correct.


Appendix C: Kubernetes Dependency Context

Kubernetes master today (as of early 2026)
───────────────────────────────────────────
go.mod:  go 1.26.0
         k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a (March 17, 2026)
         github.com/go-openapi/swag v0.25.4 // indirect (MVS-selected)

vendor/modules.txt:
  # github.com/go-openapi/swag v0.25.4
  ## explicit; go 1.24.0

vendor/github.com/go-openapi/swag/
  └─ *.go  ← v0.25.4 shim files (IsFloat64AJSONInteger delegates to conv sub-module)

vendor/k8s.io/kube-openapi/pkg/validation/validate/values.go
  └─ import "github.com/go-openapi/swag"
       └─ resolves to vendor/github.com/go-openapi/swag → v0.25.4

RESULT: CRD multipleOf validation in Kubernetes today uses
        IsFloat64AJSONInteger from swag v0.25.4 (math.Round algorithm).
        The behavioral change is already live.

After PR #590 merges and k8s updates kube-openapi:
  ← No behavioral change. kube-openapi go.mod now matches what Kubernetes
     already has in vendor. The effective code at the CRD callsite is unchanged.

Key point: Go's flat vendoring means there is only one copy of go-openapi/swag in the Kubernetes vendor directory — the MVS-selected version (v0.25.4). Both the newly updated kube-openapi and all other dependencies use the same vendored copy. PR #590's merge has zero incremental impact on CRD validation behavior.

Prompt: kube-openapi PR #590 Deep-Dive Risk Analysis

Use this prompt to regenerate the full risk-assessment markdown for kubernetes/kube-openapi PR #590.


Context

PR #590 on kube-openapi upgrades go-openapi/swag from v0.23.0 to v0.25.4, removes mailru/easyjson and josharian/intern from kube-openapi's compiled build/package dependency path (they remain in the module graph via transitive edges — go list -m all still lists them, go list -deps ./... does not), bumps the go directive from 1.23.0 to 1.24.0, and bumps go.yaml.in/yaml/v3 from v3.0.3 to v3.0.4.

The review comment triggering this analysis (from @sttts):

@dims how deep have you looked into potential behaviour changes? go-openapi has some reputation of changing semantics without notification by accident. As we use it in our CRD validation there is risk that we break our API (we have forked the go-openapi validator nowadays, so risk is lower than in the past, but worth a check anyway).

The local kube-openapi checkout is at /Users/dsrinivas/go/src/k8s.io/kube-openapi.

Key fact to verify and prominently state: Kubernetes master already vendors go-openapi/swag v0.25.4 (MVS selects it via another dependency). Go's flat vendoring means ALL imports of github.com/go-openapi/swag in the vendor tree — including from the vendored kube-openapi source — resolve to this single v0.25.4 copy. The IsFloat64AJSONInteger behavioral change is therefore already live in Kubernetes master CRD validation today, before PR #590 merges.


Instructions

Perform a thorough multi-agent investigation and produce a comprehensive markdown report (≥5 pages) saved to ~/notes/kube-openapi-pr590-risk-analysis.md.

Phase 1 — Fetch the PR

  1. Run gh pr view 590 --repo kubernetes/kube-openapi --json title,body,files,commits,author
  2. Run gh pr diff 590 --repo kubernetes/kube-openapi
  3. Run gh pr view 590 --repo kubernetes/kube-openapi --comments

Record the exact files changed, commits, and reviewer comments before proceeding.

Phase 2 — Launch parallel specialist agents (use highest-capability model, max effort)

Spawn five agents simultaneously, each focused on one angle:

Agent 1 — go-openapi/swag changelog and behavioral diff v0.23.0→v0.25.4

  • Fetch CHANGELOG/release notes: https://github.com/go-openapi/swag/releases
  • Enumerate tags between v0.23.0 and v0.25.4 via GitHub API
  • Inspect the sub-module refactoring: compare root-package files in v0.23.0 (real .go files) vs v0.25.4 (only *_iface.go shims plus doc.go and tests; non-test compatibility symbols are thin shims forwarding to sub-modules)
  • Verify the "re-exports all the same symbols" claim — check every *_iface.go
  • Fetch https://raw.githubusercontent.com/go-openapi/swag/v0.23.0/json.go and https://raw.githubusercontent.com/go-openapi/swag/v0.23.0/yaml.go to get the old implementations
  • Fetch the new sub-module sources from GitHub (or via Go module proxy) for: jsonutils, conv, jsonname, yamlutils, mangling
  • Identify ALL behavioral differences (not just API changes)
  • Pay special attention to IsFloat64AJSONInteger, ConcatJSON, WriteJSON/ReadJSON, JSONMapSlice/JSONMapItem marshaling, BytesToYAMLDoc, error messages and error wrapping
  • For ConcatJSON: note the non-container-input bug fix (old returns garbage with null byte; new returns empty string) — this is unreachable from kube-openapi but should be documented
  • For JSONMapSlice: marshal behavior differs for nil inputs ({} in v0.23.0 vs null in v0.25.4); document but assess whether kube-openapi's CRD path ever constructs a nil JSONMapSlice

Agent 2 — kube-openapi's actual usage of go-openapi/swag

  • grep -r "go-openapi/swag" /Users/dsrinivas/go/src/k8s.io/kube-openapi --include="*.go" -l
  • For each file, catalogue which swag symbols are used and how
  • Map each used symbol to its sub-module in v0.25.4
  • Read pkg/validation/validate/values.go, type.go, schema.go, result.go in full — these are the CRD validation hot paths
  • Read pkg/schemaconv/openapi.go and proto_models.go to understand the format-string changes
  • Read pkg/spec3/*.go and pkg/validation/spec/*.go for ConcatJSON usage
  • Assess test coverage: which swag-dependent paths have tests, which don't
  • Run go test ./... and go test -race ./pkg/validation/validate/... ./pkg/validation/spec/... ./pkg/spec3/... ./pkg/schemaconv/...
  • Report ALL test results including race detector output

Agent 3 — mailru/easyjson removal impact

  • How did swag v0.23.0 use easyjson? (check json.go, yaml.go)
  • What types implemented ejMarshaler/ejUnmarshaler?
  • Does kube-openapi have any types that implemented those interfaces?
  • What does the new WriteJSON do instead? (adapter pattern, stdlib default)
  • The new jsonutils adapter registry is a mutable global — document what the default adapter is, whether kube-openapi registers anything, and what a future dependency registering an adapter at init time would mean
  • Distinguish carefully: easyjson/intern are removed from go list -deps ./... (compiled build path) but still appear in go list -m all (module graph) and go.sum (go.mod hash only, not h1 source hash). State this precisely.
  • Is josharian/intern (string interning) removal safe?

Agent 4 — go.yaml.in/yaml/v3 v3.0.3→v3.0.4

  • Find the actual diff between the two tags (GitHub API or raw commit comparison)
  • Is this a source-code change or metadata-only? (Expected: only go.mod changed)
  • What is go.yaml.in/yaml/v3? Who maintains it? Is it the same as gopkg.in/yaml.v3?
  • How does kube-openapi use YAML parsing? Which files call yaml functions?
  • Check for CVEs or security advisories in this version range
  • Assess: is this bump safe for OpenAPI spec parsing and CRD schema ingestion?

Agent 5 — Kubernetes CRD validation dependency chain

  • How does Kubernetes use kube-openapi for CRD validation? Check staging/src/k8s.io/apiextensions-apiserver/
  • What version of kube-openapi does Kubernetes master currently vendor? Fetch https://raw.githubusercontent.com/kubernetes/kubernetes/master/go.mod
  • Critical: Does kubernetes/kubernetes vendor go-openapi/swag v0.25.4 directly? Check vendor/modules.txt for # github.com/go-openapi/swag. If yes, then the vendored kube-openapi source imports the root swag package which resolves to v0.25.4 — meaning the behavioral change is ALREADY LIVE in Kubernetes master CRD validation today, independent of this PR.
  • What is the "forked go-openapi validator" mentioned by @sttts? Read pkg/validation/ in kube-openapi.
  • Go 1.24.0 directive bump: Kubernetes master is already on Go 1.26.0 — this is a non-issue for Kubernetes. State the Go version explicitly. The bump matters only for standalone kube-openapi consumers (not via Kubernetes vendoring).
  • How does the sub-module split (11 new // indirect entries) affect go mod vendor?

Phase 3 — Independent cross-verification (main agent, not delegated)

While the five agents run, the main agent should independently:

  1. Inspect the shim layer:

    GOPATH=$(go env GOPATH)
    go get github.com/go-openapi/swag@v0.25.4
    ls $GOPATH/pkg/mod/github.com/go-openapi/swag@v0.25.4/
  2. Audit init-time side effects of the root import:

    # Every *_iface.go imports a sub-module — do any sub-modules have init() that
    # could cause global state mutation, I/O, or registration side effects?
    grep -rn "^func init()" $GOPATH/pkg/mod/github.com/go-openapi/swag*/
    # Expected: only jsonutils/concat.go has an init (initializes {/[ closer map)
  3. Compare IsFloat64AJSONInteger old vs new (include negative values):

    curl -s https://raw.githubusercontent.com/go-openapi/swag/v0.23.0/convert.go | grep -A 25 IsFloat64AJSONInteger
    # new version in conv sub-module
    cat $GOPATH/pkg/mod/github.com/go-openapi/swag/conv@*/convert.go | grep -A 20 IsFloat64AJSONInteger
  4. Write and run an empirical test program comparing old vs new IsFloat64AJSONInteger. The argument is always the computed quotient (in MultipleOf: 1/factor * value), not the user's raw value. Test cases must include:

    • Exact integers: 3.0, 4.0, -3.0
    • Near-integer from below (positive): 4.0 - 1e-10, 4.0 - 1e-9, 4.0 - 1e-8
    • Near-integer from above (positive): 1.0 + 1e-9
    • Negative near-integers (the algorithm is symmetric): -3.9999999999, -4.0000000001
    • 0.999999999 (near 1 from below)
    • Real multipleOf quotients: 1/0.001 * 0.9999999999 = 999.9999999, 1/0.5 * 2.4999999999 = 4.9999999998
    • Frame table by quotient passed to the function, not user value
  5. Empirically verify JSONMapSlice nil behavior:

    // Write a quick program to confirm:
    // json.Marshal(JSONMapSlice(nil)) → "{}" in v0.23.0, "null" in v0.25.4
    // json.Unmarshal([]byte("null"), &prepopulatedSlice) behavior
  6. Verify easyjson distinction (build path vs module graph):

    cd /Users/dsrinivas/go/src/k8s.io/kube-openapi
    go list -deps ./... | grep easyjson       # should be empty on PR branch
    go list -m all | grep easyjson            # will still list it (transitive graph)
    grep easyjson go.sum                       # only go.mod hash, not h1 source hash
  7. Verify no mangling functions are used:

    grep -rn "swag\.ToGoName\|swag\.ToFileName\|swag\.GoNamePrefixFunc\|swag\.AddInitialisms" \
      /Users/dsrinivas/go/src/k8s.io/kube-openapi --include="*.go"
  8. Run full test suite including race detector:

    go test ./... -count=1 -timeout 120s
    cd test/integration && go test ./...
    go test -race ./pkg/validation/validate/... ./pkg/validation/spec/... ./pkg/spec3/... ./pkg/schemaconv/...
    go vet ./pkg/validation/validate/... ./pkg/schemaconv/...
    # Note: go vet ./... may have pre-existing failures in pkg/internal/third_party/go-json-experiment/json
    # Document which failures are pre-existing vs introduced by this PR
  9. Check Kubernetes dependency status — the most important verification:

    curl -s https://raw.githubusercontent.com/kubernetes/kubernetes/master/go.mod | \
      grep -E "go-openapi/swag|kube-openapi|^go "
    # Also check vendor/modules.txt for the swag entry:
    curl -s https://raw.githubusercontent.com/kubernetes/kubernetes/master/vendor/modules.txt | \
      grep -A2 "go-openapi/swag"
    # If vendor/modules.txt shows swag v0.25.4, the behavioral change is already live
  10. Check the v0.24.0 retraction:

    cat $GOPATH/pkg/mod/github.com/go-openapi/swag@v0.25.4/go.mod | grep retract
  11. Verify yaml/v3 is purely a metadata change:

    curl -s "https://api.github.com/repos/yaml/go-yaml/compare/v3.0.3...v3.0.4" | \
      jq '.files[] | {filename, additions, deletions}'
    # Expected: only go.mod changed (go 1.22 → go 1.16)

Phase 4 — Cross-check agent findings

After all agents complete, explicitly verify consistency:

Finding Agent claim Independent verification Consistent?
IsFloat64AJSONInteger behavioral change (incl. negative values) Agent 1+2 Empirical test program ✓/✗
k8s swag v0.25.4 already live at CRD callsite Agent 5 vendor/modules.txt check ✓/✗
yaml/v3 bump is no-op Agent 4 GitHub diff ✓/✗
WriteJSON safe for kube-openapi Agent 2+3 Source inspection ✓/✗
All symbols re-exported Agent 1 File listing + iface.go inspection ✓/✗
easyjson removed from build path (not module graph) Agent 3 go list -deps vs go list -m all ✓/✗
Tests pass including race Agent 2 go test -race output ✓/✗

Flag any inconsistencies and investigate further before including in the report.

Phase 5 — Write the report

The report must cover all of the following sections in depth:

  1. Executive Summary — State upfront that the IsFloat64AJSONInteger behavioral change is already live in Kubernetes master CRD validation (via vendored swag v0.25.4). PR #590's impact is on kube-openapi module metadata and standalone consumers. Risk verdict: low.

  2. What Changed: The Full Dependency Picture

    • Sub-module architecture overview (table of 11 sub-modules + whether used by kube-openapi). Root package contains *_iface.go shims plus doc.go and tests — non-test compatibility symbols are thin shims, implementations are in sub-modules.
    • Removed dependencies: use go list -deps vs go list -m all framing. easyjson/intern removed from compiled build path; still in module graph via transitive edges from other modules (e.g. go-openapi/jsonpointer).
    • New indirect dependencies and their risk profile
    • yaml/v3 bump: zero source changes, go.mod go-directive only
  3. Code-Level Changes in kube-openapi — every changed file, nature of change, behavioral impact, and caller impact

  4. Behavioral Changes in go-openapi/swag — one subsection per function:

    • WriteJSON/ReadJSON — easyjson removal; note JSONMapSlice nil behavior change ({}null); note ConcatJSON non-container bug fix
    • IsFloat64AJSONIntegerfull algorithm comparison, frame as "nearest-integer epsilon behavior changed, symmetric around nearest integer including negative values." Show quotient-based table. Include negative examples. Do NOT frame as simply "loosened."
    • DefaultJSONNameProvider/NameProvider, Convert*, Format*, ToDynamicJSON, YAMLToJSON/BytesToYAMLDoc error wrapping
    • Root import init-time side effects: only jsonutils init (closer map), no scary globals. Adapter registry is mutable global but stdlib-default only; document as theoretical future risk.
    • GoNamePrefixFunc / name mangling — not used in kube-openapi
  5. Go 1.24.0 Directive Bump — Kubernetes master is already on Go 1.26.0 (state the exact version). This is a non-issue for Kubernetes. Real minimum floor only for standalone kube-openapi consumers.

  6. Kubernetes Dependency Chain Analysis — State clearly and first: Kubernetes master vendors swag v0.25.4 via MVS. Flat vendoring means vendored kube-openapi source already resolves to v0.25.4 at the CRD validation callsite. IsFloat64AJSONInteger change is already active. PR #590 is a metadata update for Kubernetes; no incremental behavioral change when k8s next bumps kube-openapi.

  7. v0.24.0 Retraction — tagging error, not behavioral bug

  8. Test Coverage Assessment — include race test results; note gaps: no multipleOf float boundary tests, no isFloatInt boundary tests

  9. Risk Matrix — table: change × affected code × k8s CRD impact × risk level

  10. Cross-Checking section — agent findings vs independent verification

  11. Recommendations — Use the following as the reviewer response template:

    I checked the CRD-relevant swag usage. The only behavior change I found in a CRD validation path is swag.IsFloat64AJSONInteger, used by multipleOf and float-as-integer type checks. v0.23.0 used truncation-based comparison; v0.25.4 uses nearest-integer math.Round with relative epsilon. That changes sub-epsilon boundary behavior symmetrically around integers, including negative values. 3.9999999999 and -3.9999999999 now classify as integer; 1.000000001 no longer does. Other helpers (Convert*, Format*, DefaultJSONNameProvider, ConcatJSON, ToDynamicJSON) have no CRD-relevant behavior change. JSONMapSlice nil marshaling changed but is not in the CRD path.

    Importantly, Kubernetes master already vendors swag v0.25.4, so this behavior is already active in Kubernetes master CRD validation today. Tests pass including -race for impacted packages. Low risk; suggested non-blocking follow-up: add targeted multipleOf float boundary tests.

  12. Appendix A — files changed in the PR with change nature and risk

  13. Appendix B — empirical test results for IsFloat64AJSONInteger. Table columns: quotient passed to fn | how it arises | OLD result | NEW result Include negative values. Label clearly that these are quotients, not user values.

  14. Appendix C — Kubernetes dependency context. Show that flat vendoring means swag v0.25.4 is already at the CRD callsite TODAY, before PR merges.

The report must be ≥ 5 pages (≥ 500 lines of markdown). Use tables, code blocks, and headers generously. Do not omit any section.


Output

Save the final report to:

~/notes/kube-openapi-pr590-risk-analysis.md

Then create a public GitHub Gist:

gh gist create ~/notes/kube-openapi-pr590-risk-analysis.md \
  --public \
  --desc "kube-openapi PR #590 risk analysis: go-openapi/swag v0.23.0→v0.25.4 behavioral deep-dive"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment