SeleniumHQ/selenium · error · Error::SerializationError

# ## expected # , got #

Error message

#{name}##{field.name} expected #{field.list ? 'a list' : 'a single value'}, got #{raw.inspect}

What it means

check_shape validates the JSON container shape of a BiDi field: a field declared as a list must arrive as a JSON array, and a non-list scalar/enum/ref field must NOT arrive as an array. A mismatch raises Error::SerializationError. Opaque fields (no shape descriptor) pass through unchecked.

Solutions

  1. Log the raw payload to see whether an array or scalar arrived at the failing field.
  2. Confirm the field's list/scalar cardinality in the W3C BiDi spec for your browser version.
  3. Align Selenium client and browser versions.
  4. Report the divergence with the payload.
Defensive patterns

Strategy: validation

Validate before calling

# Confirm the cardinality your code expects matches the inbound container.
def expect_list!(value, field)
  raise "#{field} expected a list" unless value.is_a?(::Array)
  value
end

Type guard

def bidi_list?(value)
  value.is_a?(::Array)
end

Try / catch

begin
  record = SomeRecord.from_json(payload)
rescue Selenium::WebDriver::Error::SerializationError => e
  raise unless e.message =~ /expected.*list|expected a single value/
  warn "cardinality mismatch: #{e.message}"
end

Prevention

When it happens

Trigger: Browser sends a JSON array for a field the schema types as a single value, or a single value/object for a field typed as a list, during inbound BiDi Record deserialization.

Common situations: Schema/browser version skew where one side treats a field as singular and the other as a list, grid rewriting arrays, spec evolution changing cardinality.

Related errors


AI-assisted analysis of SeleniumHQ/selenium@aa36b38e69 (2026-08-14). Data as JSON: /api/errors/3cb3ea3900438771. Report an issue: GitHub.

Appendix: source

Thrown at rb/lib/selenium/webdriver/bidi/serialization/record.rb:271

            # Reads a ref-typed value into its class. A `scalar` position is an inline union with
            # a scalar arm collapsed onto its union ref (a map's string keys): a non-object leaf
            # passes through instead of being handed to the object_only union, but only when it
            # matches the arm's primitive (+scalar+ carries it). A list recurses per element.
            def read_ref(field, raw)
              klass = (@refs ||= {})[field.name] ||= Protocol.const_get(field.ref)
              return read_list(field, raw, klass) if field.list

              field.scalar && !raw.is_a?(::Hash) ? scalar_value(field, raw) : klass.from_json(raw)
            end

            # A declared list must arrive as an array; a scalar-shaped field (enum or ref, not a
            # list) must not. An opaque field carries no shape descriptor, so it passes through.
            def check_shape(field, raw)
              return if field.list == raw.is_a?(::Array)
              return unless field.list || field.enum || field.ref

              raise Error::SerializationError,
                    "#{name}##{field.name} expected #{field.list ? 'a list' : 'a single value'}, got #{raw.inspect}"
            end

            # Ruby classes a checkable primitive admits. `number` is any Numeric (JSON has one
            # number type); `integer` requires an Integer — a browser emits `5`, not `5.0`, for an
            # integer (JS has no int/float split), so this rarely false-positives yet still rejects
            # a genuine non-integer like 1.5. A field with no primitive descriptor is left unchecked.
            PRIMITIVE_TYPES = {
              'string' => [::String], 'boolean' => [::TrueClass, ::FalseClass],
              'number' => [::Numeric], 'integer' => [::Integer]
            }.freeze

            def check_primitive(field, raw)
              expected = PRIMITIVE_TYPES[field.primitive]
              return if expected.nil? || expected.any? { |type| raw.is_a?(type) }

              raise Error::SerializationError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}"
            end

View on GitHub (pinned to aa36b38e69)