Exception: Axn::Extensions::Serialization::UnserializableValue

Inherits:
ArgumentError
  • Object
show all
Includes:
Axn::Error
Defined in:
lib/axn/exceptions.rb

Overview

Raised when an exposed value has no honest JSON representation, so a serializing adapter (axn-openapi, axn-mcp, axn-ruby_llm) fails the call rather than emitting garbage or a placeholder where data belongs. Six shapes, in two categories. The rendering would be WRONG, or not JSON at all: a self-referential container (no JSON representation at all), two Hash keys that stringify to one JSON property (a value silently dropped), a non-finite Float (no JSON literal exists), or a String whose bytes have no UTF-8 rendering (JSON is a UTF-8 format). The rendering would be UGLY, rejected only under serialize_value(reject_opaque: true): a value or a Hash key whose only to_s is the inherited Object#to_s, which renders an object address into a response body.

An ArgumentError so an adapter's existing rescue StandardError maps it to an error response with no adapter-side change; a SystemStackError, being outside StandardError, would escape the adapter entirely. Names the path to the offending value.

Instance Method Summary collapse

Constructor Details

#initialize(path:, value:, reason: nil) ⇒ UnserializableValue

reason: names the specific defect, punctuation included. It defaults to the cycle case — both the original meaning of this error and the only one an external caller is likely to construct — so new(path:, value:) remains a complete call.



383
384
385
386
387
388
# File 'lib/axn/exceptions.rb', line 383

def initialize(path:, value:, reason: nil)
  @path = path
  @value = value
  @reason = reason
  super()
end

Instance Method Details

#messageObject

The offending value's class is named through Internal::RenderedClassName, not @value.class: the value is caller-supplied and may override class, and running that override here would replace this failure with the value's own exception. Its bytes are foreign too — a constant may hold non-UTF-8 ones, and Module#to_s hands those back — so the name is RENDERED before it joins this message. That module composes both halves without delegating to Internal::Rendering (a require cycle) and without lifting the composition onto ClassName (which promises never to render); see its own comment. Both moves stay off limits; reaching for the shared owner is the point.

path: and reason: are rendered on the same terms, because EVERY operand of a composition owes it or none of them do. Inside the gem both are axn's own UTF-8 text (a canonicalized wire path, or an escaped spelling for a name that has no UTF-8 rendering), but this is a PUBLIC class an adapter constructs directly — new(path:, value:) is documented as a complete call — so a path in another encoding is a caller away. A raw Latin-1 path beside a raw Latin-1 class name joined fine; beside a RENDERED class name it raises Encoding::CompatibilityError from #message itself, which is the serialization failure destroyed by the report of it. Every operand normalized AT the join, including the reason — whose two sources (the caller's reason: and this class's own cycle_reason) are normalized by one call rather than one each, so which source answered cannot decide whether the message composes.



408
409
410
411
# File 'lib/axn/exceptions.rb', line 408

def message
  "Cannot serialize exposed value at `#{Axn::Internal::RenderedText.of(@path)}` (#{value_class_name}): " \
    "#{Axn::Internal::RenderedText.of(@reason || cycle_reason)}"
end