Introduction

Adapters is a high-performance, developer-friendly, and schema-driven data validation, serialization, and transformation library designed for modern Rust.

By bridging the gap between dynamic formats (like JSON or raw key-value objects) and Rust's strictly typed domain models, Adapters allows you to validate dynamic data payloads before instantiation, serialize/deserialize with full bounds checking, and easily map types between different API boundaries.


Key Pillars of the Library

The library is designed around three distinct, yet highly interconnected layers:

  • Unified Schema & Validation Layer: Instead of validating data after parsing it into structured models (which can cause panics or silent errors on invalid types), Adapters defines a declarative, dynamic schema tree. Inbound payloads are verified at the dynamic level first, matching strict type names, number ranges, string lengths, custom regexes, and complex formats (e.g., Email or URLs).

  • High-Performance Serialization & Deserialization: Adapters implements a zero-dependency serialization and deserialization model. Every primitive type (including newly added char, i128, u128, and standard network addresses like IpAddr, Ipv4Addr, and Ipv6Addr), option, vector, and map is fully supported, allowing seamless, safe round-trips from dynamic values back into complex Rust structures.

  • Functional Data Transformation: Domain models often diverge between different contexts (e.g., Database Models vs. API Presentation Models). Using Pipeline and FieldMapper classes, you can map, rename, and transform data trees programmatically in a highly functional manner.


Why Use Adapters?

  • Zero-Dependency Core Parser: Comes with a built-in recursive-descent JSON engine, avoiding bloated dependencies and guaranteeing high compilation speed.
  • Detailed Error Accumulation: The validator evaluates all constraint rules rather than short-circuiting, gathering all errors across your nested fields into a comprehensive error list in a single pass.
  • Safe Native Numeric Types: Distinct floating-point and integer tracking prevents type coercion bugs before they make it into your business logic.
  • Ergonomic Macros: Instantly implement all necessary traits for your types using simple struct tags via the #[derive(Schema)] proc-macro.

Authors & Sponsorship

Adapters is maintained by Muhammad Fiaz.

Getting Started

Learn how to install, configure, and run your first schema validation and deserialization flow using Adapters.

Installation

Since this project is under active development, referencing the official Git repository ensures you always have the latest performance improvements and features:

[dependencies]
adapters = { git = "https://github.com/muhammad-fiaz/adapters.git" }

Method 2: Manual Version Entry

Alternatively, add adapters directly as a version dependency to your Cargo.toml:

[dependencies]
adapters = "0.0.0"

Your First Schema Model

With Adapters, you define your data structures using standard Rust structs and derive their schema and validation rules directly using the #[derive(Schema)] macro.

Here is a complete, compilable example:

use adapters::prelude::*;

#[derive(Schema, Debug)]
struct UserProfile {
    #[schema(min_length = 3, max_length = 32)]
    username: String,
    
    #[schema(email)]
    email: String,
    
    #[schema(min = 18, max = 120)]
    age: u8,
    
    #[schema(optional)]
    website: Option<String>,
}

fn main() -> Result<(), adapters::Error> {
    // 1. A valid JSON payload
    let json_data = r#"{
        "username": "supercoder",
        "email": "contact@example.com",
        "age": 28,
        "website": "https://muhammad-fiaz.github.io"
    }"#;

    // 2. Parse, validate, and deserialize in a single operation!
    let user = UserProfile::from_json(json_data)?;
    println!("Successfully parsed and validated user: {:?}", user);

    // 3. Serializing a struct instance back to JSON
    let serialized_json = user.to_json()?;
    println!("Serialized JSON: {}", serialized_json);

    Ok(())
}

How It Works Under the Hood

When you call UserProfile::from_json(json_data):

  1. JSON Tokenization & Parsing: The native JSON engine parses the string into a structured Value tree.
  2. Schema Compilation: The derived SchemaProvider implementation yields the structural definition of your struct.
  3. Dynamic Validation: The SchemaValidator runs validation rules over the dynamic fields (e.g., confirming age is between 18 and 120, and email is correctly formatted).
  4. Strong Typing Deserialization: If validation passes, the deserializer converts the checked Value directly into your UserProfile Rust struct.

Schema Validation

Validation is the core pillar of Adapters. You can build validation schemas programmatically using builder types, or let the procedural macros generate them automatically.


Declarative Attributes (#[derive(Schema)])

When deriving Schema on your structures, you can use the #[schema(...)] attribute to specify validation constraints. The macro supports a rich set of rules:

String Constraints

  • min_length = <usize>: The string must contain at least N characters.
  • max_length = <usize>: The string must contain at most M characters.
  • non_empty: The string cannot be empty (equivalent to min_length = 1).
  • email: Enforces a standard RFC 5322-compliant email format check.
  • url: Enforces a valid absolute URL format check.
  • regex = "<pattern>": Matches the string against a custom regular expression.

Numeric Constraints

  • min = <number>: Value must be greater than or equal to N.
  • max = <number>: Value must be less than or equal to M.
  • positive: Enforces that the number is strictly greater than zero (> 0).
  • negative: Enforces that the number is strictly less than zero (< 0).
  • non_zero: Fails validation if the number is exactly 0.

Structural Controls

  • strict: Enforces strict type checking. When true, types like numbers will not be coerced from strings.
  • optional: Declares the field as optional (permits Null values).
  • default = <expression>: Applies a default value if the key is missing in the source payload.

Programmatic Schema Building

If you need to construct schemas dynamically at runtime, use our highly expressive builder APIs:

#![allow(unused)]
fn main() {
use adapters::prelude::*;
use adapters::schema::{ObjectSchema, StringSchema, IntegerSchema};

let schema = ObjectSchema::new()
    .field("username", StringSchema::new().required().non_empty().alphanumeric())
    .field("age", IntegerSchema::new().required().min(18).max(99).positive())
    .field("balance", IntegerSchema::new().required().non_zero())
    .strict(); // Rejects unknown object keys

// Validate a dynamic Value representation
let payload = Value::Null; // or Value::Object(...)
let result = schema.validate(&payload, "root");
}

Nested Schema Validation

Adapters natively supports recursive validation of complex nested structures. When a structure derives Schema, its schema definition incorporates the schema of any sub-structures that also implement SchemaProvider.

For example, when validating a parent struct like User, any nested objects (e.g. Address) will be fully validated against their own schemas. Any validation failures in the nested child are reported with correct dot-notation paths (e.g., address.city or address.zip_code).

#![allow(unused)]
fn main() {
use adapters::prelude::*;

#[derive(Schema, Debug)]
struct Address {
    #[schema(min_length = 3)]
    city: String,
    country: String,
}

#[derive(Schema, Debug)]
struct User {
    name: String,
    address: Address, // Automatically delegates validation to Address::schema()!
}
}

If you attempt to parse a payload where address.city is only two characters long, the validation engine will fail and report address.city as the failing field path.

Serialization & Deserialization

Adapters features highly optimized and fully type-safe serialization and deserialization traits. These traits define how memory structures are converted to and from intermediate Value dynamic trees.


The Serialize Trait

The Serialize trait defines how a typed Rust instance maps into a dynamic Value.

#![allow(unused)]
fn main() {
pub trait Serialize {
    fn serialize(&self) -> Value;
}
}

Every standard primitive type implements this out of the box:

  • Booleans (bool)
  • All integers (i8, i16, i32, i64, u8, u16, u32, u64, usize)
  • All floats (f32, f64)
  • Text types (String, &str)
  • Container types (Option<T>, Vec<T>, BTreeMap<String, T>)

The Deserialize Trait

The Deserialize trait is the inverse operation, decoding a Value tree back into a typed Rust instance while auditing numeric bounds and ranges:

#![allow(unused)]
fn main() {
pub trait Deserialize: Sized {
    fn deserialize(value: Value) -> Result<Self, Error>;
}
}

Safety and Strict Numeric Bounds

Unlike naive decoders that might cause silent overflows, Adapters performs type-safe checks during deserialization:

  • If you deserialize a value of 300 into a u8 field, it will return a clean DeserializationError explaining that 300 overflows the bounds of u8.
  • If an unsigned integer type (e.g. u32) receives a negative value (e.g. -10), it is caught and rejected immediately.

Custom Manual Implementations

While using the #[derive(Schema)] macro covers 99% of use cases, you can implement these traits manually for total control:

#![allow(unused)]
fn main() {
use adapters::{Serialize, Deserialize, Value, Error, error::DeserializationError};

struct Point {
    x: i32,
    y: i32,
}

impl Serialize for Point {
    fn serialize(&self) -> Value {
        let mut map = std::collections::BTreeMap::new();
        map.insert("x".to_string(), Value::Int(self.x as i64));
        map.insert("y".to_string(), Value::Int(self.y as i64));
        Value::Object(map)
    }
}

impl Deserialize for Point {
    fn deserialize(value: Value) -> Result<Self, Error> {
        let obj = value.as_object().ok_or_else(|| {
            DeserializationError::new("expected an object representation")
        })?;
        
        let x = obj.get("x")
            .and_then(|v| v.as_int())
            .ok_or_else(|| DeserializationError::new("missing coordinate x"))? as i32;
            
        let y = obj.get("y")
            .and_then(|v| v.as_int())
            .ok_or_else(|| DeserializationError::new("missing coordinate y"))? as i32;

        Ok(Point { x, y })
    }
}
}

Nested/Recursive Serialization & Deserialization

Adapters fully supports deep nested models under both programmatic use and standard macro derivation.

When you define a structure that references another structure as a field (both having derived Schema), the serialization and deserialization routines operate recursively:

  1. Nested Serialization: Translates the complex model tree from the nested structures all the way down into deep hierarchical key-value JSON trees (Value::Object).
  2. Nested Deserialization: Reconstructs custom nested Rust models dynamically from the intermediate hierarchy, handling all type verification and bounds checking nested deep inside the child structures.
#![allow(unused)]
fn main() {
use adapters::prelude::*;

#[derive(Schema, Debug)]
struct Metadata {
    created_at: String,
    version: String,
}

#[derive(Schema, Debug)]
struct Post {
    title: String,
    metadata: Metadata, // Automatically maps, validates, and serializes recursively!
}
}

Data Transformation

Data models often look completely different depending on the layer they occupy (e.g. database schema vs. public HTTP response payload). Adapters provides functional, highly efficient tools to transform data dynamically.


The Adapt Trait

The Adapt trait represents a standard pattern for mapping one typed model structure into another:

#![allow(unused)]
fn main() {
pub trait Adapt<T>: Sized {
    fn adapt(value: T) -> Result<Self, Error>;
}
}

Typical Use Case: DB Model -> API Response

#![allow(unused)]
fn main() {
use adapters::prelude::*;

struct UserRow {
    id: i64,
    db_hash: String,
    username: String,
}

#[derive(Debug)]
struct UserAPIResponse {
    id: i64,
    username: String,
}

impl Adapt<UserRow> for UserAPIResponse {
    fn adapt(row: UserRow) -> Result<Self, Error> {
        Ok(UserAPIResponse {
            id: row.id,
            username: row.username,
        })
    }
}
}

Transformation Pipelines

The Pipeline builder lets you sequence separate, dynamic mapping operations over Value trees. If any step fails, the entire chain short-circuits safely.

#![allow(unused)]
fn main() {
use adapters::prelude::*;

let transform_pipeline = Pipeline::new()
    // Step 1: Multiply integers by 2
    .step(|val| match val {
        Value::Int(n) => Ok(Value::Int(n * 2)),
        other => Ok(other),
    })
    // Step 2: Add 1 to the result
    .step(|val| match val {
        Value::Int(n) => Ok(Value::Int(n + 1)),
        other => Ok(other),
    });

let result = transform_pipeline.run(Value::Int(5))?;
assert_eq!(result, Value::Int(11)); // 5 * 2 + 1 = 11
}

Field Renaming (FieldMapper)

When interacting with external services, key casings often differ (e.g. snake_case vs. camelCase). The FieldMapper renames fields inside nested object values dynamically.

#![allow(unused)]
fn main() {
use adapters::prelude::*;

let mapper = FieldMapper::new()
    .map("first_name", "firstName")
    .map("last_name", "lastName");

let mut input_map = std::collections::BTreeMap::new();
input_map.insert("first_name".to_string(), Value::String("Alice".into()));
input_map.insert("last_name".to_string(), Value::String("Smith".into()));
input_map.insert("age".to_string(), Value::Int(30));

let input = Value::Object(input_map);
let output = mapper.apply(&input)?;

// output key structure is now:
// { "firstName": "Alice", "lastName": "Smith", "age": 30 }
}

API Configs & Reference

This page provides a comprehensive breakdown of the core traits, types, custom configurations, and errors exposed by the Adapters public API.


Unified Configurations & Traits

Adapter

The central trait combining validation, serialization, deserialization, and schema introspection.

#![allow(unused)]
fn main() {
pub trait Adapter: Serialize + Deserialize + Validate + SchemaProvider + Sized {
    fn from_json(json: &str) -> Result<Self, Error>;
    fn to_json(&self) -> Result<String, Error>;
    fn from_value(value: Value) -> Result<Self, Error>;
    fn to_value(&self) -> Value;
    fn is_valid(&self) -> bool;
}
}

SchemaProvider

Allows types to expose their structural validation specifications dynamically at runtime.

#![allow(unused)]
fn main() {
pub trait SchemaProvider {
    fn schema() -> Schema;
}
}

Dynamic Type Reference: Value

The Value enum represents dynamic, JSON-compatible, or runtime-defined data structures:

#![allow(unused)]
fn main() {
pub enum Value {
    Null,
    Bool(bool),
    Int(i64),
    Float(f64),
    String(String),
    Array(Vec<Value>),
    Object(BTreeMap<String, Value>),
}
}

Utility Type-Safe Extractors

  • as_str() -> Option<&str>
  • as_int() -> Option<i64>
  • as_float() -> Option<f64>
  • as_bool() -> Option<bool>
  • as_array() -> Option<&Vec<Value>>
  • as_object() -> Option<&BTreeMap<String, Value>>
  • is_null() -> bool

Complete Macro Reference

The procedural macro #[derive(Schema)] automatically implements SchemaProvider, Serialize, Deserialize, Validate, and Adapter on target structures.

Field Attributes Listing

Configure your struct fields using the #[schema(...)] helper:

Attribute RuleSupported TypesAction Description
min_length = <usize>StringEnforces a minimum string character count.
max_length = <usize>StringEnforces a maximum string character count.
non_emptyStringRestricts string to be non-empty (minimum 1 character).
alphanumericStringEnforces only alphanumeric characters.
emailStringMatches the string value against standard RFC 5322 format.
urlStringMatches the string value against standard URL layout.
regex = "<pattern>"StringValidates string matching using custom Rust regex.
min = <number>All numbersRestricts numbers to be greater than or equal to value.
max = <number>All numbersRestricts numbers to be less than or equal to value.
positiveAll numbersChecks if numbers are strictly positive ($>0$).
negativeAll numbersChecks if numbers are strictly negative ($<0$).
non_zeroAll numbersRestricts numbers to exclude exact $0$ value.
optionalAll typesDeclares the field is non-required and defaults to null.
strictAll typesOpts into strict validation: no implicit type coercions.
default = <expr>All typesPopulates field with expression value when key is absent.

Error Handling Types

Every fallible operation returns a Result<T, Error>. The top-level Error enum covers:

#![allow(unused)]
fn main() {
pub enum Error {
    /// Validation constraint violated.
    Validation(ValidationError),
    /// Failure during struct-to-Value serialization.
    Serialization(SerializationError),
    /// Failure during Value-to-struct deserialization.
    Deserialization(DeserializationError),
    /// Lexing or parsing failures inside the native JSON engine.
    Json(JsonError),
    /// Structural Schema error.
    Schema(SchemaError),
}
}

Usage Examples

Welcome to the examples section for Adapters! These compilable walkthroughs showcase different core features and capabilities of the library:

1. Basic Schema Validation & Serialization

Demonstrates dynamic validation, struct mapping via simple tags, and round-trips from JSON using #[derive(Schema)].

2. Explicit Schema Builder

Learn how to define structural constraints dynamically at runtime without derive macros.

3. Advanced Validation Constraints

Explore full constraint capabilities like non-empty, alphanumeric, positive/negative bounds, and non-zero check rules.

4. Native JSON Parsing Engine

Showcases native, highly-optimized zero-dependency tokenization, unicode escapes, and pretty-print JSON formatting.

5. Deep Nested Object Models

Validate recursive children structures and gather accurate validation error paths (e.g., address.city).

6. Optional Fields & Default Values

Leverage Option fields and handle default fallback values programmatically.

7. Strict Type Coercion Mode

Prevent implicit casting or conversion of numeric and string types by opting into strict mode.

8. Data Transformation & Pipelines

Transform and adapt domain structures between database layouts and web responses.

Basic Schema Validation & Serialization

This example demonstrates how to use the declarative #[derive(Schema)] macro to define metadata, run dynamic JSON validation, self-validate instances, and output serialized JSON strings.

Compilable Example

//! basic_schema example — demonstrates #[derive(Schema)], from_json, validate, to_json.

use adapters::{Adapter, SchemaProvider, Validate};
use adapters_macros::Schema;

#[derive(Schema, Debug)]
struct User {
    #[schema(min_length = 3, max_length = 32)]
    username: String,
    #[schema(email)]
    email: String,
    #[schema(min = 18, max = 120)]
    age: u8,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== basic_schema example ===\n");

    // Valid user
    let json = r#"{"username": "alice", "email": "alice@example.com", "age": 25}"#;
    let user = User::from_json(json)?;
    println!("Parsed: {:?}", user);

    user.validate()?;
    println!("Valid!");

    let out = user.to_json()?;
    println!("Serialized: {}", out);

    let schema = User::schema();
    println!("Schema type: {:?}", schema);

    // Validation failure
    println!("\n--- Testing bad input ---");
    let bad_result = User::from_json(r#"{"username": "ab", "email": "not-an-email", "age": 10}"#);
    match bad_result {
        Ok(_) => println!("Unexpectedly parsed"),
        Err(e) => println!("Validation error (expected): {}", e),
    }

    Ok(())
}

Explicit Schema Builder

This walkthrough demonstrates how to build and validate structured configurations programmatically at runtime using the Schema object builder API without depending on macro derivation.

Compilable Example

//! explicit_schema example — demonstrates ObjectSchema builder without derive macro.

use adapters::json::parse;
use adapters::{Schema, SchemaValidator, Value};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== explicit_schema example ===\n");

    // Build a schema for a user object without any derive macros
    let user_schema = Schema::object()
        .field(
            "username",
            Schema::string().required().min_length(3).max_length(32),
        )
        .field("email", Schema::string().required().email())
        .field("age", Schema::integer().required().min(18).max(120))
        .field("bio", Schema::string().optional());

    // Validate a valid JSON value
    let json = r#"{"username":"bob","email":"bob@example.com","age":30}"#;
    let value = parse(json)?;
    match user_schema.validate(&value, "root") {
        Ok(()) => println!("Valid user object!"),
        Err(e) => println!("Validation error: {}", e),
    }

    // Validate an invalid value
    let bad_json = r#"{"username":"x","email":"notanemail","age":15}"#;
    let bad_value = parse(bad_json)?;
    match user_schema.validate(&bad_value, "root") {
        Ok(()) => println!("Valid (unexpected)"),
        Err(e) => println!("Error (expected): {}", e),
    }

    // Build a standalone string schema
    let email_schema = Schema::string().required().email().min_length(5);
    let email_val = Value::String("test@example.com".into());
    let bad_email = Value::String("notvalid".into());

    println!(
        "\nEmail '{}': {:?}",
        email_val.as_str().unwrap(),
        Schema::String(email_schema)
            .validate(&email_val, "email")
            .map(|_| "valid")
    );
    let email_schema2 = Schema::string().required().email().min_length(5);
    println!(
        "Email 'notvalid': {:?}",
        Schema::String(email_schema2)
            .validate(&bad_email, "email")
            .err()
            .map(|e| e.to_string())
    );

    Ok(())
}

Advanced Validators

This example showcases modern complex constraints (like non_empty, alphanumeric, positive, negative, and non_zero) implemented via tags or dynamic programmatic builders.

Compilable Example

//! Advanced validation rules example showcasing `non_empty`, `alphanumeric`,
//! `positive`, `negative`, and `non_zero` validation rules both via declarative
//! macros and programmatic builders.

use adapters::SchemaValidator;
use adapters::prelude::*;
use adapters::schema::{FloatSchema, IntegerSchema, ObjectSchema, StringSchema};

/// A financial invoice transaction derived using macro constraints.
#[derive(Schema, Debug)]
struct TransactionInvoice {
    /// Reference code must be alphanumeric and non-empty.
    #[schema(non_empty, alphanumeric)]
    reference_code: String,

    /// Number of items must be strictly positive (> 0).
    #[schema(positive)]
    quantity: i32,

    /// Adjustments or discounts must be strictly negative (< 0.0).
    #[schema(negative)]
    discount: f32,

    /// Balance cannot be zero (!= 0).
    #[schema(non_zero)]
    balance: i64,
}

fn main() -> Result<(), Error> {
    println!("=== 1. Declarative Macro Validation ===");

    // Valid Payload
    let valid_json = r#"{
        "reference_code": "INV2026TX",
        "quantity": 5,
        "discount": -15.50,
        "balance": 250
    }"#;
    let invoice = TransactionInvoice::from_json(valid_json)?;
    println!(
        "Successfully parsed and validated valid invoice:\n{:#?}\n",
        invoice
    );

    // Invalid Payload with multiple constraint violations
    let invalid_json = r#"{
        "reference_code": "INV-2026-TX!",
        "quantity": 0,
        "discount": 5.50,
        "balance": 0
    }"#;

    match TransactionInvoice::from_json(invalid_json) {
        Ok(_) => println!("Error: Expected validation failure but succeeded!"),
        Err(e) => {
            println!("Validation failed as expected! Errors:");
            println!("{}", e);
        }
    }

    println!("\n=== 2. Programmatic Builder Validation ===");

    // Build the identical validation logic programmatically
    let invoice_schema = ObjectSchema::new()
        .field(
            "reference_code",
            StringSchema::new().required().non_empty().alphanumeric(),
        )
        .field("quantity", IntegerSchema::new().required().positive())
        .field("discount", FloatSchema::new().required().negative())
        .field("balance", IntegerSchema::new().required().non_zero());

    // Validate a dynamic Value payload using our schema
    let dynamic_invalid_payload = Value::Object(
        [
            ("reference_code".to_string(), Value::String("".to_string())), // Fails non_empty
            ("quantity".to_string(), Value::Int(-10)),                     // Fails positive
            ("discount".to_string(), Value::Float(-5.0)),                  // Passes negative
            ("balance".to_string(), Value::Int(0)),                        // Fails non_zero
        ]
        .into_iter()
        .collect(),
    );

    match invoice_schema.validate(&dynamic_invalid_payload, "invoice") {
        Ok(_) => println!("Error: Expected programmatic validation failure!"),
        Err(err) => {
            println!("Programmatic validation detected constraint violations successfully:");
            println!(
                "Field: {}, Code: {}, Message: {}",
                err.field, err.code, err.message
            );
        }
    }

    Ok(())
}

JSON Parsing

This example demonstrates how to use the built-in, native, zero-dependency recursive-descent JSON tokenization parser and serializer engine.

Compilable Example

//! json_parsing example — native JSON parse and stringify.

use adapters::Value;
use adapters::json::{parse, stringify, stringify_pretty};
use std::collections::BTreeMap;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== json_parsing example ===\n");

    // All value types
    let samples = [
        "null",
        "true",
        "false",
        "42",
        "-7",
        "3.14",
        r#""hello world""#,
        "[]",
        "{}",
        r#"[1, "two", null, false, 3.14]"#,
        r#"{"name":"alice","age":30,"active":true}"#,
    ];

    for s in &samples {
        let v = parse(s)?;
        let out = stringify(&v)?;
        println!("Input:  {s}");
        println!("Parsed: {:?}", v);
        println!("Output: {out}\n");
    }

    // Unicode strings
    let unicode = r#""\u0048\u0065\u006C\u006C\u006F \u4e16\u754c""#;
    let v = parse(unicode)?;
    println!("Unicode: {} → {:?}\n", unicode, v);

    // Empty containers
    let empty_obj = parse("{}")?;
    let empty_arr = parse("[]")?;
    println!("Empty object: {:?}", empty_obj);
    println!("Empty array:  {:?}\n", empty_arr);

    // Pretty print
    let mut m = BTreeMap::new();
    m.insert("name".to_string(), Value::String("Bob".into()));
    m.insert(
        "scores".to_string(),
        Value::Array(vec![Value::Int(10), Value::Int(20)]),
    );
    let obj = Value::Object(m);
    println!("Pretty printed:\n{}", stringify_pretty(&obj)?);

    // Error case
    match parse("{invalid}") {
        Ok(_) => println!("Unexpected success"),
        Err(e) => println!("Parse error (expected): {}", e),
    }

    Ok(())
}

Nested Models

This example outlines recursive struct configurations. Sub-elements are fully validated, and dynamic errors accumulate precise dot-notation paths (e.g. address.city).

Compilable Example

//! nested_models example — nested structs with validation error path reporting.

use adapters::{Adapter, Validate};
use adapters_macros::Schema;

#[derive(Schema, Debug)]
struct Address {
    city: String,
    country: String,
}

#[derive(Schema, Debug)]
struct User {
    username: String,
    address: Address,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== nested_models example ===\n");

    // Full round-trip
    let json = r#"{
        "username": "alice",
        "address": {"city": "Berlin", "country": "Germany"}
    }"#;

    let user = User::from_json(json)?;
    println!("Parsed user: {:?}", user);

    let out = user.to_json()?;
    println!("Re-serialized: {}", out);

    // Parse back
    let user2 = User::from_json(&out)?;
    println!("Round-trip OK: username={}", user2.username);

    // Validation
    user.validate()?;
    println!("Valid!");

    Ok(())
}

Optional Fields & Default Values

Demonstrates how to handle missing structural values in fields using Option<T> or define fallbacks dynamically via declarative metadata attributes.

Compilable Example

//! optional_defaults example — Option<T> fields and default values.

use adapters::{Adapter, Validate};
use adapters_macros::Schema;

#[derive(Schema, Debug)]
struct Profile {
    name: String,
    bio: Option<String>,
    #[schema(default = "India")]
    country: String,
    #[schema(default = 0)]
    score: i32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== optional_defaults example ===\n");

    // All fields present
    let full = Profile::from_json(
        r#"{
        "name": "Alice",
        "bio": "Rust enthusiast",
        "country": "USA",
        "score": 42
    }"#,
    )?;
    println!("Full: {:?}", full);

    // bio absent → None, country absent → default "India", score absent → default 0
    let minimal = Profile::from_json(r#"{"name": "Bob"}"#)?;
    println!("Minimal: {:?}", minimal);
    assert!(minimal.bio.is_none());
    assert_eq!(minimal.country, "India");
    assert_eq!(minimal.score, 0);
    println!("bio is None: {}", minimal.bio.is_none());
    println!("country default: {}", minimal.country);
    println!("score default: {}", minimal.score);

    // Explicit null for bio → None
    let with_null = Profile::from_json(r#"{"name": "Carol", "bio": null}"#)?;
    println!("\nWith explicit null bio: {:?}", with_null);
    assert!(with_null.bio.is_none());

    // Validate
    full.validate()?;
    minimal.validate()?;
    println!("\nAll valid!");

    Ok(())
}

Strict Mode Type Validation

This example outlines type strictness settings. When active, implicit conversions/coercions between numeric representations or string mappings are explicitly blocked.

Compilable Example

//! strict_mode example — demonstrates strict type validation.

use adapters::{Adapter, Schema as SchemaApi, SchemaValidator, Value};
use adapters_macros::Schema;

#[derive(Schema, Debug)]
struct Payment {
    #[schema(strict)]
    amount: f64,
    #[schema(strict)]
    currency: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== strict_mode example ===\n");

    // Valid strict input
    let valid = Payment::from_json(r#"{"amount": 18.5, "currency": "USD"}"#)?;
    println!("Valid payment: {:?}", valid);

    // Strict: string "18.5" should fail for f64 amount
    println!("\n--- Testing strict rejections ---");
    let bad1 = Payment::from_json(r#"{"amount": "18.5", "currency": "USD"}"#);
    match bad1 {
        Err(e) => println!("String for amount rejected (expected): {}", e),
        Ok(p) => println!("String for amount accepted (unexpected): {:?}", p),
    }

    // Strict: integer 42 should fail for string currency
    let bad2 = Payment::from_json(r#"{"amount": 10.0, "currency": 42}"#);
    match bad2 {
        Err(e) => println!("Int for currency rejected (expected): {}", e),
        Ok(p) => println!("Int for currency accepted (unexpected): {:?}", p),
    }

    // Manual schema: non-strict (default)
    println!("\n--- Non-strict schema (coercion allowed) ---");
    let loose_schema = SchemaApi::object()
        .field("amount", SchemaApi::float())
        .field("currency", SchemaApi::string());

    let coerced = Value::Object({
        let mut m = std::collections::BTreeMap::new();
        m.insert("amount".into(), Value::String("99.9".into()));
        m.insert("currency".into(), Value::Int(1));
        m
    });

    match loose_schema.validate(&coerced, "root") {
        Ok(()) => println!("Non-strict accepts coercible types (expected)"),
        Err(e) => println!("Non-strict failed unexpectedly: {}", e),
    }

    Ok(())
}

Data Transformation & Pipeline

This walkthrough details how to use the pipeline components, dynamic mappers, and the Adapt trait to translate data representations securely across interfaces.

Compilable Example

//! Transformation example showcasing the Adapt trait, Pipeline, and FieldMapper.

use adapters::{Adapt, Error, FieldMapper, Pipeline, Value};
use std::collections::BTreeMap;

struct Celsius(f64);
struct Fahrenheit(f64);

impl Adapt<Celsius> for Fahrenheit {
    fn adapt(c: Celsius) -> Result<Self, Error> {
        Ok(Fahrenheit(c.0 * 9.0 / 5.0 + 32.0))
    }
}

fn make_obj(fields: &[(&str, Value)]) -> Value {
    let mut m = BTreeMap::new();
    for (k, v) in fields {
        m.insert(k.to_string(), v.clone());
    }
    Value::Object(m)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== transformation example ===\n");

    let f = Fahrenheit::adapt(Celsius(100.0))?;
    println!("100°C = {}°F", f.0);

    let pipeline = Pipeline::new()
        .step(|v| match v {
            Value::Int(n) => Ok(Value::Int(n * 2)),
            other => Ok(other),
        })
        .step(|v| match v {
            Value::Int(n) => Ok(Value::Int(n + 10)),
            other => Ok(other),
        });

    let result = pipeline.run(Value::Int(5))?;
    println!("Pipeline 5 → {}", result);

    let mapper = FieldMapper::new()
        .map("first_name", "firstName")
        .map("last_name", "lastName")
        .map("phone_number", "phoneNumber");

    let db_row = make_obj(&[
        ("first_name", Value::String("Alice".into())),
        ("last_name", Value::String("Smith".into())),
        ("phone_number", Value::String("+1-555-0100".into())),
        ("id", Value::Int(42)),
    ]);

    let api_response = mapper.apply(&db_row)?;
    println!("\nDB row:       {}", db_row);
    println!("API response: {}", api_response);

    Ok(())
}