# `AgentBlueprintProtocol.Json`
[🔗](https://github.com/baselabs/agent_blueprint_protocol/blob/v0.1.1/lib/agent_blueprint_protocol/json.ex#L1)

Bounded JSON decoder over Erlang/OTP's `:json.decode/3` custom-decoder hook.

It produces a closed, tagged value algebra with order-preserving objects;
rejects duplicate members and trailing non-whitespace; enforces every parse
ceiling (`AgentBlueprintProtocol.Bounds`) and the I-JSON integer magnitude
bound `±(2^53−1)`; and never raises to the caller on malformed input (an
internal invariant violation still fails loud — see the decode catch). There
is exactly one decoder in the package — every artifact and every conformance
file passes through here.

**Integer window (the amendment of 2026-08-21):** a pure-digit lexeme
above `±(2^53−1)` admits as `{:float, f}` — the correctly-rounded double
`Float.parse` yields, which is what an ECMAScript peer parses from the same
digits — iff that double's canonical ECMAScript serialization
(`AgentBlueprintProtocol.Canonicalization.number/1`) reproduces the lexeme
byte-exactly; every other above-bound integer lexeme denies with
`:number_not_double_expressible`. The bound's stated purpose is JS-peer
round-trip: exact doubles round-trip, silently-lossy spellings do not.
Core fields stay fail-closed against window floats — amended 2026-08-21
by design: under the frozen 2020-12 dialect semantics
(`Schema`), `{"type":"integer"}` matches zero-fraction numbers tag-blind
(validation §6.1.1; the integer/float tag does not survive the wire, so
no non-Elixir verifier can implement tag-narrowed integer). The core-field
deny therefore lives at the ARTIFACT field typing (the registry
engine, which sees the tag), not at the schema layer. Posture unchanged
— mechanism relocated.

Errors are **value-free**: a reason names a category, never the offending
bytes or values.
The tagged value algebra is decode output — it never authorizes anything.
The tagged value algebra is decode output — it never authorizes anything.

# `reason`

```elixir
@type reason() ::
  :invalid_syntax
  | :invalid_encoding
  | :invalid_number
  | :number_not_double_expressible
  | :duplicate_member
  | :trailing_bytes
  | :invalid_type
  | AgentBlueprintProtocol.Bounds.error()
```

# `value`

```elixir
@type value() ::
  :null
  | {:boolean, boolean()}
  | {:integer, integer()}
  | {:float, float()}
  | {:string, binary()}
  | {:array, [value()]}
  | {:object, [{binary(), value()}]}
```

# `decode`

```elixir
@spec decode(binary(), AgentBlueprintProtocol.Bounds.t() | map()) ::
  {:ok, value()} | {:error, reason()}
```

Decode `input` under `bounds` (a `Bounds` struct or a tighten-only overrides
map, defaulting to the profile maxima). Returns the tagged value or a
value-free error.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
