Summary
axe tap --id <any-id> fails with Error: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil)) whenever the rendered AX tree contains any element with role: AXSlider (SwiftUI Slider, Picker(.wheel) over UIPickerView, UISlider directly). The error is identifier-agnostic — every identifier in the tree fails the same way until the slider is removed.
axe describe-ui works on the same tree (it dumps raw JSON without going through the AccessibilityElement decoder), so the failure is in the JSON model, not in FBSimulatorControl.
Repro
Minimal SwiftUI iOS app, root view:
import SwiftUI
@main
struct App: App {
var body: some Scene { WindowGroup { Root() } }
}
struct Root: View {
@State private var x: Double = 0.5
var body: some View {
VStack(spacing: 24) {
Text("Hello").accessibilityIdentifier("title")
Button("Tap") {}.accessibilityIdentifier("btn")
Slider(value: $x, in: 0...1) // remove this and tap_id works
}.padding()
}
}
Build, install on a booted iPhone simulator (tested: iPhone 17 / iOS 26.3 / Xcode 26.3), launch, then:
$ axe tap --id title --udid <udid>
Error: typeMismatch(Swift.Dictionary<Swift.String, Any>, ...)
$ axe tap --id btn --udid <udid>
Error: typeMismatch(Swift.Dictionary<Swift.String, Any>, ...)
Remove the Slider(...) line and both calls return exit 0.
Cause
Sources/AXe/Utilities/AccessibilityElement.swift:32:
iOS's accessibility serializer emits AXValue as a JSON Number for AXSlider-role elements (Float for SwiftUI Slider / UISlider, Int for UIPickerView-backed Picker(.wheel)). JSONDecoder throws typeMismatch against String? on that field for the slider element, which aborts the whole array decode.
The error message is misleading because of the swallow-and-retry in Sources/AXe/Utilities/AccessibilityFetcher.swift:40-50:
if let roots = try? decoder.decode([AccessibilityElement].self, from: jsonData) {
return roots
}
let root = try decoder.decode(AccessibilityElement.self, from: jsonData) // surfaces a "Dictionary vs Array" error
The first decode is where the real Number → String? mismatch happens; the try? discards that error, and the second decode produces the surfaced "Expected to decode Dictionary<String, Any> but found an array instead" — which sent at least one investigation off the trail.
Other findings worth noting from the same investigation:
UIPageControl shares the AXSlider role but emits AXValue as a String (e.g., 'page 3 of 5') and does NOT poison. So the trigger is the JSON type of AXValue, not the role.
ProgressView, Stepper, Toggle, UIDatePicker all emit String AXValue and are safe.
- iOS's accessibility serializer for
UISlider ignores a Swift-side accessibilityValue: String? override and reads value: Float directly — so adopters can't fix this from their app's side via the documented accessibilityValue protocol.
Suggested fix
Make AXValue a polymorphic field that decodes String, Number, or null:
let AXValue: AXValueField?
enum AXValueField: Decodable {
case string(String)
case number(Double)
init(from decoder: Decoder) throws {
let c = try decoder.singleValueContainer()
if let s = try? c.decode(String.self) { self = .string(s); return }
if let d = try? c.decode(Double.self) { self = .number(d); return }
throw DecodingError.typeMismatch(
AXValueField.self,
.init(codingPath: decoder.codingPath,
debugDescription: "AXValue is neither String nor Number"))
}
var stringValue: String {
switch self {
case .string(let s): return s
case .number(let d):
return d.truncatingRemainder(dividingBy: 1) == 0 ? String(Int(d)) : String(d)
}
}
}
Plus normalizedValue becomes AXValue?.stringValue.trimming....
Worth noting: the try? swallow on AccessibilityFetcher.swift:44 is locally unreachable for fetchAccessibilityElements (which always passes point: nil, returning array shape) but encodes a real polymorphism for DescribeUI.swift's --point path (which returns dict shape). Don't drop it naively. A cleaner path is to peek the first non-whitespace JSON byte and dispatch explicitly:
let first = jsonData.first(where: { !$0.isASCIIWhitespace })
switch first {
case UInt8(ascii: "["): return try decoder.decode([AccessibilityElement].self, from: jsonData)
case UInt8(ascii: "{"): return [try decoder.decode(AccessibilityElement.self, from: jsonData)]
default: throw CLIError(errorDescription: "Unexpected accessibility JSON shape")
}
This honors the documented dual-shape contract while surfacing the real Number → String? typeMismatch instead of masking it as a Dictionary-vs-Array mismatch. Same pattern would apply to Tests/TestUtilities.swift:157-175.
Environment
- AXe
1.6.0 (Homebrew) / repo HEAD 550b3ab
- iPhone 17 simulator, iOS 26.3, Xcode 26.3
- macOS 15 (Darwin 24.6.0)
Summary
axe tap --id <any-id>fails withError: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))whenever the rendered AX tree contains any element withrole: AXSlider(SwiftUISlider,Picker(.wheel)over UIPickerView,UISliderdirectly). The error is identifier-agnostic — every identifier in the tree fails the same way until the slider is removed.axe describe-uiworks on the same tree (it dumps raw JSON without going through theAccessibilityElementdecoder), so the failure is in the JSON model, not in FBSimulatorControl.Repro
Minimal SwiftUI iOS app, root view:
Build, install on a booted iPhone simulator (tested: iPhone 17 / iOS 26.3 / Xcode 26.3), launch, then:
Remove the
Slider(...)line and both calls return exit 0.Cause
Sources/AXe/Utilities/AccessibilityElement.swift:32:iOS's accessibility serializer emits
AXValueas a JSON Number forAXSlider-role elements (Float for SwiftUISlider/UISlider, Int forUIPickerView-backedPicker(.wheel)).JSONDecoderthrowstypeMismatchagainstString?on that field for the slider element, which aborts the whole array decode.The error message is misleading because of the swallow-and-retry in
Sources/AXe/Utilities/AccessibilityFetcher.swift:40-50:The first decode is where the real
Number → String?mismatch happens; thetry?discards that error, and the second decode produces the surfaced "Expected to decode Dictionary<String, Any> but found an array instead" — which sent at least one investigation off the trail.Other findings worth noting from the same investigation:
UIPageControlshares theAXSliderrole but emitsAXValueas a String (e.g.,'page 3 of 5') and does NOT poison. So the trigger is the JSON type ofAXValue, not the role.ProgressView,Stepper,Toggle,UIDatePickerall emit StringAXValueand are safe.UISliderignores a Swift-sideaccessibilityValue: String?override and readsvalue: Floatdirectly — so adopters can't fix this from their app's side via the documentedaccessibilityValueprotocol.Suggested fix
Make
AXValuea polymorphic field that decodes String, Number, or null:Plus
normalizedValuebecomesAXValue?.stringValue.trimming....Worth noting: the
try?swallow onAccessibilityFetcher.swift:44is locally unreachable forfetchAccessibilityElements(which always passespoint: nil, returning array shape) but encodes a real polymorphism forDescribeUI.swift's--pointpath (which returns dict shape). Don't drop it naively. A cleaner path is to peek the first non-whitespace JSON byte and dispatch explicitly:This honors the documented dual-shape contract while surfacing the real
Number → String?typeMismatch instead of masking it as a Dictionary-vs-Array mismatch. Same pattern would apply toTests/TestUtilities.swift:157-175.Environment
1.6.0(Homebrew) / repo HEAD550b3ab