The Unknown Type — Unvalidated Wire Data

Unknown is the type for data that hasn't been validated yet. Think of it as a typed serde_json::Value — it can hold any shape of data, but you can't do anything useful with it until you run it through a schema.

Creating Unknown Values

#![allow(unused)]
fn main() {
use id_effect::schema::Unknown;

// From a JSON string
let u: Unknown = Unknown::from_json_str(r#"{"name": "Alice", "age": 30}"#)?;

// From a serde_json Value
let v: serde_json::Value = serde_json::json!({ "name": "Alice" });
let u: Unknown = Unknown::from_serde_json(v);

// From raw parts
let u: Unknown = Unknown::object([
    ("name", Unknown::string("Alice")),
    ("age",  Unknown::integer(30)),
]);

// Primitives
let s: Unknown = Unknown::string("hello");
let n: Unknown = Unknown::integer(42);
let b: Unknown = Unknown::boolean(true);
let null: Unknown = Unknown::null();
let arr: Unknown = Unknown::array([Unknown::integer(1), Unknown::integer(2)]);
}

Why Not serde_json::Value Directly?

serde_json::Value is an excellent data type, but it's stringly typed: value["name"] gives you an Option<&Value> and there's no structure around parse errors, path tracking, or accumulation. Unknown wraps the same idea but integrates with id_effect's schema parser, which gives you:

  • Path tracking — "error at .users[3].email"
  • Accumulated errors — all failures in one parse, not just the first
  • Composable schemas — build complex validators from simple primitives

Inspecting Unknown Values

You don't normally inspect Unknown directly — you run it through a schema. But when debugging:

#![allow(unused)]
fn main() {
// Check what shape the value has
match u.kind() {
    UnknownKind::Object(fields) => { /* … */ }
    UnknownKind::Array(elems)   => { /* … */ }
    UnknownKind::String(s)      => { /* … */ }
    UnknownKind::Integer(n)     => { /* … */ }
    UnknownKind::Float(f)       => { /* … */ }
    UnknownKind::Boolean(b)     => { /* … */ }
    UnknownKind::Null            => { /* … */ }
}

// Access a field without parsing (returns Option<&Unknown>)
let name: Option<&Unknown> = u.field("name");
}

The Parse Boundary

Unknown is your import type. At every IO boundary — HTTP handler, NATS message, config file, database row — convert incoming data to Unknown first, then parse it with a schema:

#![allow(unused)]
fn main() {
async fn handle_request(body: Bytes) -> Effect<CreateUserResponse, ApiError, Deps> {
    effect! {
        // Convert raw bytes to Unknown
        let raw = Unknown::from_json_bytes(&body)
            .map_err(ApiError::InvalidJson)?;

        // Parse Unknown into a typed, validated struct
        let req: CreateUserRequest = ~ parse_schema(create_user_schema(), raw);

        // Now req is fully trusted — proceed with domain logic
        ~ create_user(req)
    }
}
}

Nothing beyond the parse boundary sees Unknown. Domain functions only accept validated types.

Unknown and Serde

If you have existing serde-deserializable types, use the serde bridge (requires the schema-serde feature):

#![allow(unused)]
fn main() {
use id_effect::schema::serde_bridge::unknown_from_serde_json;

// Deserialise via serde, then convert to Unknown for schema validation
let value: serde_json::Value = serde_json::from_str(input)?;
let u: Unknown = unknown_from_serde_json(value);
}

This lets you incrementally adopt the schema system without rewriting all your serde impls at once.