Nested arrayContains reifies to null in generated body (bundled pact_ffi 0.4.7)
#133 aperta il 15 lug 2026
Metriche repository
- Star
- (55 star)
- Metriche merge PR
- (Metriche PR in attesa)
Descrizione
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 fromPactSwiftMockServer.xcframework) - Dependency chain:
PactSwift→PactSwiftServer 0.4.7→PactMockServer 0.1.2(thelibpact_ffixcframework) - Discovered while using a fork that adds an
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 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
childrenisnull. - Expected:
childrenis 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.