SurrealDB Docs Logo

Enter a search query

Flexible typing

Most examples in the Rust SDK feature strict types like the following that can be serialized and deserialized as needed.

#[derive(Debug, Serialize, Deserialize)] struct Student { name: String, class_id: u32, }

However, sometimes you will need to work with types that have a more dynamic structure. This page offers a few methods to use in such a case.

Getting started

Start a running database using the following command:

surreal start --user root --pass root

To follow along interactively, connect using Surrealist or the following command to open up the CLI:

surrealdb % surreal sql --user root --pass root --ns namespace --db database --pretty

Then use the cargo add command to add four crates: surrealdb, serde, serde_json, and tokio. Your Cargo.toml file should look something like this.

[dependencies] serde = "1.0.214" serde_json = "1.0.132" surrealdb = "2.0.4" tokio = "1.41.0"

Strict vs. flexible typing

The following example is a typical one, featuring a Student struct that holds a name and a class_id, followed by the .select() function to display all the students.

use serde::{Deserialize, Serialize}; use surrealdb::engine::remote::ws::Ws; use surrealdb::opt::auth::Root; use surrealdb::{Error, Surreal}; #[derive(Debug, Serialize, Deserialize)] struct Student { name: String, class_id: u32, } #[tokio::main] async fn main() -> Result<(), Error> { // Connect to the database server let db = Surreal::new::<Ws>("localhost:8000").await?; // Sign in into the server db.signin(Root { username: "root", password: "root", }) .await?; // Select the namespace and database to use db.use_ns("namespace").use_db("database").await?; let all_students: Vec<Student> = db.select("student").await?; println!("All students: {all_students:?}"); Ok(()) }

Before running this code, first use Surrealist or the CLI to populate the database with two students.

CREATE student SET name = "Student 1", class_id = 10; CREATE student SET name = "Another student", class_id = 20;

Once that is done, running cargo run will display the following output.

All students: [Student { name: "Another student", class_id: 20 }, Student { name: "Student 1", class_id: 10 }]

So far so good, but what if we were still experimenting with the data and created a student that diverged from the Student struct?

CREATE student SET name = "Third student", class_id = 40, metadata = { teacher: teacher:mr_gundry_white, favourite_classes: ["Music", "Industrial arts"] };

In this case the output would still conform to the Student struct and the extra information in the third student would not show up.

All students: [Student { name: "Another student", class_id: 20 }, Student { name: "Student 1", class_id: 10 }, Student { name: "Third student", class_id: 40 }]

One possibility here is to use the .query() method, which will always return the output as received from the database.

use serde::{Deserialize, Serialize}; use surrealdb::engine::remote::ws::Ws; use surrealdb::opt::auth::Root; use surrealdb::{Error, Surreal}; #[derive(Debug, Serialize, Deserialize)] struct Student { name: String, class_id: u32, } #[tokio::main] async fn main() -> Result<(), Error> { // Connect to the database server let db = Surreal::new::<Ws>("localhost:8000").await?; // Sign in into the server db.signin(Root { username: "root", password: "root", }) .await?; // Select the namespace and database to use db.use_ns("namespace").use_db("database").await?; let all_students = db.query("SELECT * FROM student").await?; println!("All students: {all_students:?}"); Ok(()) }

However, the output is a bit noisy.

All students: Response { client: Surreal { router: OnceLock(Router { sender: Sender { .. }, last_id: 4, features: {LiveQueries} }), engine: PhantomData<surrealdb::api::engine::any::Any> }, results: {0: (Stats { execution_time: Some(65.417µs) }, Ok(Array(Array([Object(Object({"class_id": Number(Int(20)), "id": Thing(Thing { tb: "student", id: String("7bhlb23ti1vedykpsnzd") }), "name": Strand(Strand("Another student"))})), Object(Object({"class_id": Number(Int(10)), "id": Thing(Thing { tb: "student", id: String("rpi0qmsqc7rwaxddfxpb") }), "name": Strand(Strand("Student 1"))})), Object(Object({"class_id": Number(Int(40)), "id": Thing(Thing { tb: "student", id: String("xl5rzvlkghtn01nh5tw2") }), "metadata": Object(Object({"favourite_classes": Array(Array([Strand(Strand("Music")), Strand(Strand("Industrial arts"))])), "teacher": Thing(Thing { tb: "teacher", id: String("mr_gundry_white") })})), "name": Strand(Strand("Third student"))}))]))))}, live_queries: {} }

A better solution when working with dynamic structures in a situation like this is to pass in a Resource into the methods we use. Database methods that take a Resource will automatically return a Value which contains an enum of all the possible data types in SurrealDB, and thus does not require deserializing. In addition, a Value' implementsDisplay`, making the output similar to that in the CLI.

use serde::{Deserialize, Serialize}; use surrealdb::engine::remote::ws::Ws; use surrealdb::opt::auth::Root; use surrealdb::opt::Resource; use surrealdb::{Error, Surreal}; #[derive(Debug, Serialize, Deserialize)] struct Student { name: String, class_id: u32, } #[tokio::main] async fn main() -> Result<(), Error> { // Connect to the database server let db = Surreal::new::<Ws>("localhost:8000").await?; // Sign in into the server db.signin(Root { username: "root", password: "root", }) .await?; // Select the namespace and database to use db.use_ns("namespace").use_db("database").await?; let all_students = db.select(Resource::from("student")).await?; println!("All students debug: {all_students:?}\n"); println!("All students display: {all_students}"); Ok(()) }

As the output shows, Debug printing returns an output similar to the .query() example above, while using Display is much neater.

All students debug: Array(Array([Object(Object({"class_id": Number(Int(20)), "id": Thing(Thing { tb: "student", id: String("7bhlb23ti1vedykpsnzd") }), "name": Strand(Strand("Another student"))})), Object(Object({"class_id": Number(Int(10)), "id": Thing(Thing { tb: "student", id: String("rpi0qmsqc7rwaxddfxpb") }), "name": Strand(Strand("Student 1"))})), Object(Object({"class_id": Number(Int(40)), "id": Thing(Thing { tb: "student", id: String("xl5rzvlkghtn01nh5tw2") }), "metadata": Object(Object({"favourite_classes": Array(Array([Strand(Strand("Music")), Strand(Strand("Industrial arts"))])), "teacher": Thing(Thing { tb: "teacher", id: String("mr_gundry_white") })})), "name": Strand(Strand("Third student"))}))])) All students display: [{ class_id: 20, id: student:7bhlb23ti1vedykpsnzd, name: 'Another student' }, { class_id: 10, id: student:rpi0qmsqc7rwaxddfxpb, name: 'Student 1' }, { class_id: 40, id: student:xl5rzvlkghtn01nh5tw2, metadata: { favourite_classes: ['Music', 'Industrial arts'], teacher: teacher:mr_gundry_white }, name: 'Third student' }]

A Value from serde_json can be passed into functions like .create(), allowing the json! macro to also be used.

use serde::{Deserialize, Serialize}; use serde_json::json; use surrealdb::engine::remote::ws::Ws; use surrealdb::opt::auth::Root; use surrealdb::opt::Resource; use surrealdb::{Error, Surreal}; #[derive(Debug, Serialize, Deserialize)] struct Student { name: String, class_id: u32, } #[tokio::main] async fn main() -> Result<(), Error> { // Connect to the database server let db = Surreal::new::<Ws>("localhost:8000").await?; // Sign in into the server db.signin(Root { username: "root", password: "root", }) .await?; // Select the namespace and database to use db.use_ns("namespace").use_db("database").await?; let new_student = db.create(Resource::from("student")).content(json!({ "age": 15, "weekly_allowance": 20.5 })).await?; println!("{new_student}"); Ok(()) }

Output:

{ age: 15, id: student:gp6g0p7t23musi3ms77d, weekly_allowance: 20.5f }

On this page

© SurrealDB GitHub Discord Community Cloud Features Releases Install