surpher/PactSwift

Nested arrayContains reifies to null in generated body (bundled pact_ffi 0.4.7)

Open

#133 geöffnet am 15. Juli 2026

Auf GitHub ansehen
 (0 Kommentare) (0 Reaktionen) (0 zugewiesene Personen)Swift (19 Forks)auto 404
Trivialgood first issue

Repository-Metriken

Stars
 (55 Stars)
PR-Merge-Metriken
 (PR-Metriken ausstehend)

Beschreibung

Summary

An arrayContains matcher that is nested inside another arrayContains reifies to null in the generated mock-server body, instead of to a concrete array. Strict consumer-side decoding of that body then fails. Top-level arrayContains works correctly; only nesting is affected.

The root cause is in the bundled pact_ffi (0.4.7), not in the Swift layer.

Environment

  • Bundled FFI: pact_ffi 0.4.7, pact_models 1.1.9 (read from PactSwiftMockServer.xcframework)
  • Dependency chain: PactSwiftPactSwiftServer 0.4.7PactMockServer 0.1.2 (the libpact_ffi xcframework)
  • Discovered while using a fork that adds an arrayContains matcher (pepejeria/PactSwift@feat/array-contains-matcher), but the underlying defect is in the bundled FFI and affects the whole surpher stack.

Root cause

In pact_ffi 0.4.7, rust/pact_ffi/src/mock_server/bodies.rsprocess_object, the example body for an arrayContains matcher is generated by re-processing its variants array with skip_matchers = true:

let (value, skip_matchers) = if let Ok(rule) = &matching_rule {
  match rule {
    MatchingRule::ArrayContains(_) => (obj.get("variants"), true), // reprocess variants, skip_matchers = true
    _ => (obj.get("value"), false)
  }
} ...;

Under skip_matchers = true, any object carrying pact:matcher:type is reified only from its "value" key:

} else { // skip_matchers == true
  match obj.get("value") {
    Some(val) => ...,
    None => Value::Null   // <-- an arrayContains object has no "value" -> null
  }
}

An arrayContains matcher serialises as { "pact:matcher:type": "arrayContains", "variants": [...] } with no "value" key. So:

  • Top-level arrayContains works — reached via the non-skip path, which reads variants.
  • Nested arrayContains collapses to null — reached only via the skip-matchers path, which looks for "value" and finds none.

Already fixed upstream

In pact_ffi ≥ 0.4.9, process_matcher was refactored so the arrayContains branch recursively builds its example from variants and returns Value::Array(json_values), independent of any "value" key. On that version nested arrayContains works with no client change. So bumping the bundled libpact_ffi (via PactMockServer / PactSwiftServer) to ≥ 0.4.9 is the clean fix.

Reproduction

An end-to-end consumer test with a nested arrayContains, run against the real mock server:

try response.jsonBody(
    .arrayContains([
        [
            "id": .like("outer-1"),
            "children": .arrayContains([
                ["childId": .like("inner-1")],
            ]),
        ],
    ])
)

Fetch the generated body inside builder.verify { ... } and inspect it:

  • Actual (0.4.7): the top-level array is present, but children is null.
  • Expected: children is a non-empty array ([{ "childId": "inner-1" }]).

Proposed fixes

Bump the bundled libpact_ffi to ≥ 0.4.9 in PactMockServer / PactSwiftServer, where arrayContains example generation is recursive and no longer depends on a "value" key.

Contributor Guide