vld-leptos
Leptos integration for the vld validation library.
Define validation rules once — use them on both server and client (WASM).
No direct dependency on leptos — works with any Leptos version (0.6, 0.7, 0.8+) and compiles for WASM targets.
Installation
[dependencies]
vld = "0.1"
vld-leptos = "0.1"
leptos = "0.7"
Quick Start
1. Shared Validation Schemas
Define schema factories in a shared module compiled for both server and WASM:
pub fn name_schema() -> vld::primitives::ZString {
vld::string().min(2).max(50)
}
pub fn email_schema() -> vld::primitives::ZString {
vld::string().email()
}
pub fn age_schema() -> vld::primitives::ZInt {
vld::number().int().min(0).max(150)
}
2. Server Function Validation
Use validate_args! inside #[server] functions:
use leptos::prelude::*;
#[server]
async fn create_user(
name: String,
email: String,
age: i64,
) -> Result<(), ServerFnError> {
vld_leptos::validate_args! {
name => shared::name_schema(),
email => shared::email_schema(),
age => shared::age_schema(),
}
.map_err(|e| ServerFnError::new(e.to_string()))?;
Ok(())
}
3. Client-Side Reactive Validation
Use check_field inside Leptos memos for instant feedback:
#[component]
fn CreateUserForm() -> impl IntoView {
let (name, set_name) = signal(String::new());
let (email, set_email) = signal(String::new());
let name_err = Memo::new(move |_| {
let v = name.get();
if v.is_empty() { return None; } vld_leptos::check_field(&v, &shared::name_schema())
});
let email_err = Memo::new(move |_| {
let v = email.get();
if v.is_empty() { return None; }
vld_leptos::check_field(&v, &shared::email_schema())
});
view! {
<form>
<input
type="text"
placeholder="Name"
on:input=move |ev| set_name.set(event_target_value(&ev))
/>
<Show when=move || name_err.get().is_some()>
<span class="error">{move || name_err.get().unwrap_or_default()}</span>
</Show>
<input
type="email"
placeholder="Email"
on:input=move |ev| set_email.set(event_target_value(&ev))
/>
<Show when=move || email_err.get().is_some()>
<span class="error">{move || email_err.get().unwrap_or_default()}</span>
</Show>
<button type="submit">"Create"</button>
</form>
}
}
4. Server → Client Error Display
Parse structured errors returned from server functions:
#[component]
fn CreateUserForm() -> impl IntoView {
let action = ServerAction::<CreateUser>::new();
let server_errors = Memo::new(move |_| {
action.value().get().and_then(|result| {
result.err().and_then(|e| {
vld_leptos::VldServerError::from_json(&e.to_string())
})
})
});
view! {
<ActionForm action>
<input type="text" name="name" />
<Show when=move || {
server_errors.get()
.map(|e| e.has_field_error("name"))
.unwrap_or(false)
}>
<span class="error">
{move || server_errors.get()
.and_then(|e| e.field_error("name").map(String::from))
.unwrap_or_default()}
</span>
</Show>
<input type="email" name="email" />
<button type="submit">"Create"</button>
</ActionForm>
}
}
API Reference
Error Types
| Type |
Description |
VldServerError |
Structured error with per-field messages, serializable for transport |
FieldError |
Single field error: { field, message } |
VldServerError Methods
| Method |
Returns |
Description |
validation(fields) |
VldServerError |
Create from a list of FieldError |
internal(msg) |
VldServerError |
Create an internal error |
field_error(name) |
Option<&str> |
First error message for a field |
field_errors(name) |
Vec<&str> |
All error messages for a field |
has_field_error(name) |
bool |
Check if a field has errors |
error_fields() |
Vec<&str> |
All field names with errors |
from_json(s) |
Option<Self> |
Parse from JSON string |
to_string() |
String |
Serialize to JSON (via Display) |
Validation Functions
| Function |
Use Case |
validate::<Schema, T>(data) |
Validate a Serialize struct against a vld::schema! type |
validate_value::<Schema>(json) |
Validate a serde_json::Value directly |
check_field(value, schema) |
Single-field check → Option<String> error |
check_field_all(value, schema) |
Single-field check → Vec<String> all errors |
check_all_fields::<Schema, T>(data) |
Multi-field check → Vec<FieldError> |
Macros
| Macro |
Description |
validate_args! { field => schema, ... } |
Inline validation of server function arguments |
Custom Error Type
For advanced use cases, wrap VldServerError in your own error enum:
use serde::{Deserialize, Serialize};
use server_fn::codec::JsonEncoding;
use leptos::server_fn::error::{FromServerFnError, ServerFnErrorErr};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AppError {
Validation(vld_leptos::VldServerError),
NotFound(String),
Internal(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Validation(e) => write!(f, "{}", e),
AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
AppError::Internal(msg) => write!(f, "Internal: {}", msg),
}
}
}
impl std::error::Error for AppError {}
impl FromServerFnError for AppError {
type Encoder = JsonEncoding;
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
AppError::Internal(value.to_string())
}
}
#[server]
async fn create_user(name: String) -> Result<(), AppError> {
vld_leptos::validate_args! {
name => vld::string().min(2),
}
.map_err(AppError::Validation)?;
Ok(())
}
Schema-Based Alternative
Instead of individual schema functions, use a vld::schema! struct:
vld::schema! {
struct CreateUserSchema {
name: String => vld::string().min(2).max(50),
email: String => vld::string().email(),
}
}
#[derive(Serialize)]
struct FormData { name: String, email: String }
let data = FormData { name, email };
vld_leptos::validate::<CreateUserSchema, _>(&data)?;
let errors = vld_leptos::check_all_fields::<CreateUserSchema, _>(&data);
Running the Example
cargo run -p vld-leptos --example leptos_basic
License
MIT