Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Macros

Use ConvexValue for Rust types you write yourself—application error payloads, patch objects, or v.any() results you decode by hand. Generated models already derive it; you only need the macro when you add your own structs or enums.

Convert between your types and SDK values with the runtime codec:

use convex::Value;
use convex_typegen_runtime::{ConvexValue, value_serde::{from_value, to_value}};

#[derive(ConvexValue)]
struct Patch {
    title: String,
}

let wire: Value = to_value(&Patch { title: "Draft".into() })?;
let patch: Patch = from_value(wire)?;

The derive implements Serde with Convex wire rules. Pair it with to_value / from_value rather than serde_json when talking to convex::Value.

Include generated bindings

Use the runtime’s include!() macro to load generated Rust in a module after Cargo runs the build script. See include bindings from Cargo output for the default and custom filename forms.

Derive a Convex model

Use ConvexValue when you control a wire shape outside the generated validators, for example an application error payload, an adapter around v.any(), or a patch model. For an arbitrary result represented by RawValue, pass its inner SDK value (raw.0) to from_value::<YourModel>(raw.0) once you know its shape.

The example below encodes a patch into an SDK Value, checks the exact wire keys and values, and decodes it back. Use the tutorial manifest, save the file as src/bin/wire.rs, and run cargo run --bin wire. No backend is needed; success means all assertions pass. The runtime supplies the derive and the codec, so this example needs no direct Serde dependency.

use std::collections::BTreeMap;

use convex::Value;
use convex_typegen_runtime::ConvexValue;
use convex_typegen_runtime::value_serde::{from_value, to_value};

#[derive(Debug, PartialEq, ConvexValue)]
enum Priority
{
    #[convex(literal = "low")]
    Low,
    #[convex(literal = "high")]
    High,
}

#[derive(Debug, PartialEq, ConvexValue)]
struct TaskPatch
{
    #[convex(rename = "taskText")]
    text: String,
    #[convex(optional)]
    priority: Option<Option<Priority>>,
}

fn main() -> Result<(), Box<dyn std::error::Error>>
{
    for (priority, wire_priority) in [
        (None, None),
        (Some(None), Some(Value::Null)),
        (Some(Some(Priority::High)), Some(Value::String("high".into()))),
    ] {
        let patch = TaskPatch {
            text: "Write Rust".into(),
            priority,
        };
        let mut expected = BTreeMap::from([("taskText".into(), Value::String("Write Rust".into()))]);
        if let Some(value) = wire_priority {
            expected.insert("priority".into(), value);
        }

        // Encode an SDK value, then decode it back into the handwritten model.
        let wire = to_value(&patch)?;
        assert_eq!(wire, Value::Object(expected));
        assert_eq!(from_value::<TaskPatch>(wire)?, patch);
    }
    Ok(())
}

Supported attributes and shapes

  • Named-field structs encode as objects and reject unknown fields on decode. Field names default to the Rust spelling, with the r# prefix removed.
  • Use #[convex(optional)] on Option<T> to omit None and decode a present key as T. An unmarked Option<T> is required and nullable. An optional field can also accept null when its inner type accepts null, such as the inner Option<Priority> above.
  • Use i64 for Int64, f64 for Float64, Bytes for bytes, and BTreeMap<K, V> with string-shaped keys for records.
  • Use a unit struct with #[convex(literal = "created")] for an exact wire literal. Booleans, signed 64-bit integers, and floats also work. Write 1 or 1_i64 for Int64 and 1.0 for Float64. The generator can use #[convex(float_bits = ...)] to preserve an exact Float64 bit pattern.
  • For a string literal enum, annotate each unit variant with a distinct #[convex(literal = "...")] string. The enum encodes as that exact string.
  • Tuple variants encode the payload of a single-field tuple variant directly. Decoding tries variants in declaration order. For a discriminated union, set #[convex(discriminator = "kind")] on the enum and #[convex(tag = CreatedTag)] on each variant, using its distinct literal type for the required kind field. The decoder selects the branch by that wire field and validates the whole payload.

The derive supports concrete types. Tuple structs, Rust unions, and variants with unsupported field shapes produce compile-time diagnostics. For generated function calls, use the typed references under bindings::api.

Why generation stays in the build script

The derive sees only the Rust item it annotates. The generator reads TypeScript or a committed function spec offline. Keeping those inputs in build.rs lets Cargo rebuild when they change and gives rust-analyzer a generated Rust file to navigate. Neither macro runs generation or touches the network.