BoundaryML/baml · error · JwtError

JSON error: {0}

Error message

JSON error: {0}

What it means

JwtError::JsonError converts a serde_json::Error (via #[from]) when deserializing decoded JWT segments (header/claims) into structs fails. After base64 decoding, the bytes must be valid JSON matching the expected shape.

Source

Thrown at engine/baml-runtime/src/internal/wasm_jwt.rs:29

use base64::{
    engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
    Engine,
};
use js_sys::{Array, Object, Uint8Array};
use serde::{Deserialize, Serialize};
use serde_json::json;
use thiserror::Error;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::{window, CryptoKey, SubtleCrypto};

#[derive(Error, Debug)]
pub enum JwtError {
    #[error("JavaScript error: {0:?}")]
    JsError(JsValue),
    #[error("Base64 decode error: {0}")]
    Base64Error(#[from] base64::DecodeError),
    #[error("JSON error: {0}")]
    JsonError(#[from] serde_json::Error),
    #[error("Missing window object")]
    NoWindow,
    #[error("Missing crypto API")]
    NoCrypto,
}

impl From<JsValue> for JwtError {
    fn from(err: JsValue) -> Self {
        JwtError::JsError(err)
    }
}

pub async fn encode_jwt(
    claims: &serde_json::Value,
    private_key_pem: &str,
) -> Result<String, JwtError> {
    // Extract the crypto.subtle API

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Deserialize into serde_json::Value first and inspect the actual claims structure.
  2. Make serde struct fields Option or add #[serde(default)] for claims that may be absent.
  3. Verify the token payload decodes to JSON (e.g. echo in a JWT debugger).
  4. Align struct field names with the token via #[serde(rename)] attributes.

Example fix

// before
struct Claims { sub: String, admin: bool }
// after
#[derive(Deserialize)]
struct Claims { sub: String, #[serde(default)] admin: Option<bool> }
Defensive patterns

Strategy: type-guard

Validate before calling

const claims = JSON.parse(atobUrlSafe(payloadSegment));
if (typeof claims.sub !== "string") throw new Error("Missing sub claim");

Type guard

function hasRequiredClaims(c) {
  return typeof c?.sub === "string" && typeof c?.exp === "number";
}

Try / catch

match serde_json::from_slice::<Claims>(&payload) {
    Ok(claims) => Ok(claims),
    Err(e) => {
      let raw: serde_json::Value = serde_json::from_slice(&payload)?;
      eprintln!("Unexpected claims: {}", raw);
      Err(e.into())
    }
}

Prevention

When it happens

Trigger: Decoding a JWT whose payload segment is not valid JSON, or whose claims lack fields required by the target serde struct (missing/renamed keys, wrong types).

Common situations: Token from a nonstandard issuer with unexpected claim names; using a struct requiring fields the token omits; decoding garbage bytes; expiry/claims type mismatches (string vs number).

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12). Data as JSON: /api/errors/25d9ed86d597e49e. Report an issue: GitHub.