#JSON Outcomes Cookbook

Version: 0.43.0 | Updated: 2026-03-25 | Applies to: ranvier-http 0.43+ | Category: Cookbook


#Overview

v0.43 introduces typed JSON auto-serialization at the HTTP route boundary. Transitions return domain structs โ€” JSON serialization is infrastructure, aligned with PHILOSOPHY.md Section 5: "Infrastructure as Boundary."

Example: typed-json-api (in examples/typed-json-api/)


#1. `get_json_out` โ€” GET with Typed JSON Response

Register a GET route where Outcome<T, E> is auto-serialized to JSON:

use ranvier_http::prelude::*;
use ranvier_runtime::Axon;

#[derive(Serialize, Deserialize)]
struct HealthResponse {
    status: String,
    version: String,
}

let health = Axon::simple::<String>("health").then(HealthCheck);

Ranvier::http()
    .get_json_out("/api/health", health)  // Outcome<HealthResponse, String> โ†’ JSON
    .run(())
    .await

Outcome::Next(HealthResponse { .. }) โ†’ 200 OK with JSON body. Outcome::Fault(e) โ†’ 500 Internal Server Error.


#2. `post_typed_json_out` โ€” Typed Input + Typed Output

Combines post_typed() (JSON body auto-deserialization) with JSON auto-serialization:

#[derive(Serialize, Deserialize, JsonSchema)]
struct CreateItemInput {
    name: String,
    price: f64,
}

#[derive(Serialize, Deserialize)]
struct Item {
    id: String,
    name: String,
    price: f64,
}

let create = Axon::typed::<CreateItemInput, String>("create").then(CreateItem);

Ranvier::http()
    .post_typed_json_out("/api/items", create)
    .run(())
    .await
  • Input type (CreateItemInput): requires Deserialize + JsonSchema
  • Output type (Item): requires Serialize
  • No manual serde_json::to_string() or serde_json::from_str() anywhere

#3. `delete_json_out` โ€” DELETE with Typed Response

#[derive(Serialize, Deserialize)]
struct DeleteResult { deleted: bool, id: String }

let delete = Axon::simple::<String>("delete").then(DeleteItem);

Ranvier::http()
    .delete_json_out("/api/items/:id", delete)
    .run(())
    .await

Also available: put_typed_json_out(), patch_typed_json_out().


#4. `BusHttpExt` โ€” Path and Query Parameter Extraction

v0.43 adds the BusHttpExt trait for ergonomic HTTP parameter access:

use ranvier_http::BusHttpExt;  // or via prelude

// Path parameter: /api/items/:id
let id: Uuid = bus.path_param("id")?;  // Result<T: FromStr, String>

// Query parameter: /api/items?page=2
let page: Option<i64> = bus.query_param("page");  // Option<T: FromStr>

// Query with default: /api/items?per_page=10
let per_page: i64 = bus.query_param_or("per_page", 10);  // T: FromStr

#Before / After

// BEFORE (v0.42): manual PathParams extraction
let id: u64 = match bus.read::<PathParams>().and_then(|p| p.get("id")) {
    Some(raw) => match raw.parse() {
        Ok(id) => id,
        Err(_) => return Outcome::Fault("Invalid ID".into()),
    },
    None => return Outcome::Fault("Missing ID".into()),
};

// AFTER (v0.43): BusHttpExt::path_param()
let id: u64 = match bus.path_param("id") {
    Ok(id) => id,
    Err(e) => return Outcome::Fault(e),
};

#5. `Bus::get_cloned()` โ€” Concise Resource Extraction

// BEFORE: read() returns Option<&T>, need .cloned()
let pool = bus.read::<PgPool>().cloned()
    .ok_or_else(|| "PgPool not in Bus")?;

// AFTER: get_cloned() returns Result<T, BusAccessError>
let pool = bus.get_cloned::<PgPool>().expect("PgPool");

// Or with try_outcome! for Outcome context:
let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool not in Bus");

#6. `json_outcome()` โ€” Manual JSON when Needed

When a route uses .get() (not get_json_out) and you need JSON output manually:

use ranvier_http::prelude::json_outcome;

async fn run(&self, _input: (), _res: &(), _bus: &mut Bus) -> Outcome<String, String> {
    let report = Report { title: "demo".into(), count: 3 };
    json_outcome(&report)  // Outcome<String, String>
}

json_outcome() serializes to Outcome::Next(json_string). On serialization error: Outcome::Fault(error_message).


#7. `outcome_to_json_response()` โ€” Low-Level Control

For custom middleware or manual response construction:

use ranvier_http::outcome_to_json_response;

let response: HttpResponse = outcome_to_json_response(outcome);
// Next(T) โ†’ 200 OK with JSON
// Fault(E) โ†’ 500 with debug message

#8. Route Method Summary

Method Input Output Use Case
get_json_out () Outcome<T, E> โ†’ JSON Read endpoints
post_typed_json_out T: JsonSchema Outcome<U, E> โ†’ JSON Create with typed body
put_typed_json_out T: JsonSchema Outcome<U, E> โ†’ JSON Update with typed body
patch_typed_json_out T: JsonSchema Outcome<U, E> โ†’ JSON Partial update
delete_json_out () Outcome<T, E> โ†’ JSON Delete endpoints
get / post varies Outcome<String, E> Manual serialization

#See Also

  • Outcome Patterns Cookbook โ€” try_outcome!, combinators
  • Bus Access Patterns Cookbook โ€” Bus::get_cloned(), BusHttpExt
  • examples/typed-json-api/ โ€” complete CRUD with typed JSON routes
  • ranvier-fullstack-reference/ โ€” production-like deployment with typed JSON