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:
PactSwift → PactSwiftServer 0.4.7 → PactMockServer 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.rs → process_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.
Summary
An
arrayContainsmatcher that is nested inside anotherarrayContainsreifies tonullin the generated mock-server body, instead of to a concrete array. Strict consumer-side decoding of that body then fails. Top-levelarrayContainsworks correctly; only nesting is affected.The root cause is in the bundled
pact_ffi(0.4.7), not in the Swift layer.Environment
pact_ffi 0.4.7,pact_models 1.1.9(read fromPactSwiftMockServer.xcframework)PactSwift→PactSwiftServer 0.4.7→PactMockServer 0.1.2(thelibpact_ffixcframework)arrayContainsmatcher (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.rs→process_object, the example body for anarrayContainsmatcher is generated by re-processing itsvariantsarray withskip_matchers = true:Under
skip_matchers = true, any object carryingpact:matcher:typeis reified only from its"value"key:An
arrayContainsmatcher serialises as{ "pact:matcher:type": "arrayContains", "variants": [...] }with no"value"key. So:variants.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_matcherwas refactored so the arrayContains branch recursively builds its example fromvariantsand returnsValue::Array(json_values), independent of any"value"key. On that version nested arrayContains works with no client change. So bumping the bundledlibpact_ffi(viaPactMockServer/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:Fetch the generated body inside
builder.verify { ... }and inspect it:childrenisnull.childrenis a non-empty array ([{ "childId": "inner-1" }]).Proposed fixes
Bump the bundled
libpact_ffito ≥ 0.4.9 inPactMockServer/PactSwiftServer, where arrayContains example generation is recursive and no longer depends on a"value"key.