Skip to content
You are reading v0.1, which is not the current release. Go to v0.4.

JSON APIs

Use typed request and response records at the HTTP boundary. std::json parses wire text into Json, a derived schema reads a Khora type out of it and reports every problem, and Encode writes a typed value back for the response.

This service accepts POST /users with a JSON body such as {"name":"Ada"} and returns a typed user as JSON:

module main;
import std::core::{ChildFailed, Result, SharedFn, Validated};
import std::json::{parse};
import std::net::http::{HttpError, Request, Response, Router};
import std::schema::{Decode, Encode, Raw, Rejection};
derive(Decode)
pub type CreateUser = {
name: String,
};
derive(Encode)
pub type User = {
id: Int,
name: String,
};
fn create_user(request: Request) -> Response {
let document = match parse(request.body) {
Result::Err(_) =>
return Response::text(400, "body is not valid JSON"),
Result::Ok(value) => value,
};
let input = match CreateUser::schema().decode(Raw::of_json(document)) {
Validated::Invalid(problems) =>
return Response::json(422, problems),
Validated::Valid(input) => input,
};
let user: User = {
id: 1,
name: input.name,
};
Response::json(201, user)
}
pub fn main()
raises HttpError + ChildFailed
{
Router::new()
|> Router::post("/users", SharedFn::of(create_user))
|> Router::listen(8080)!
}

The boundary handles two distinct failures separately. parse returns Result::Err when the body is not JSON at all, so the handler answers 400. decode answers Validated::Invalid when the document is valid JSON but has the wrong shape for CreateUser, so the handler answers 422 — with every problem, because a Rejection encodes as an object with its path and its message, and a client wants the list rather than the first line of it:

[{"message":"name should be text, and is 7","path":"name"}]

Derive when the wire shape matches the type

Section titled “Derive when the wire shape matches the type”

These declarations:

derive(Decode)
pub type CreateUser = {
name: String,
};

ask the compiler to write the schema from the type. This is the right default when the API representation intentionally matches the Khora record.

When the external API has different field names, a rule that is part of the wire contract, or a legacy shape, implement Decode for that record by hand with a struct({ .. }) literal, and every schema that contains it picks the impl up through the trait. Decode untrusted input has the whole story.

Parsing and decoding are separate operations

Section titled “Parsing and decoding are separate operations”

Keep this distinction visible:

let document = parse(request.body); // Result<Json, JsonError>
let input = CreateUser::schema().decode(Raw::of_json(document)); // Validated<CreateUser, Rejection>

A malformed document and a well-formed document with the wrong fields are different client mistakes. Keeping them separate also gives an API boundary enough information to choose different responses without string-parsing an error message.

Response::json takes anything that implements Encode: a derived record, a Json built by hand, a list of rejections.

Response::json(201, user)

A record holding a Redacted has no Encode, so it cannot be sent by accident; the build stops at the derive line.

For more complex response policies you can call std::json::encode on Raw::to_json(value.encode()) directly and construct the HTTP response explicitly.

For the complete JSON surface, see the JSON API reference. For HTTP routing and request data, see HTTP service.

Khora 0.1.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource