> ## Documentation Index
> Fetch the complete documentation index at: https://rydantic.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Your first data contract

> Turn messy signup JSON into a validated Rust value and inspect failures field by field.

Build a signup model that accepts surrounding whitespace and a numeric age string, normalizes the email, fills optional values and rejects unknown fields.

10 minutesCore RydanticNo external services

## 1. Create a project

```sh theme={"system"}
cargo new signup-contract
cd signup-contract
```

Add this dependency to the generated manifest:

```toml Cargo.toml theme={"system"}
[dependencies]
rydantic = "=0.7.0"
```

## 2. Define the boundary

The `Signup` struct in the complete source below is the contract. `#[derive(Model)]` supplies parsing, serialization and schema generation.

| Declaration | Effect |
| - | - |
| `#[model(extra = "forbid")]` | Report unknown input fields |
| `trim` and `lowercase` | Normalize text before applying its constraints |
| `email` | Validate an email address |
| `coerce, ge = 18, le = 120` | Accept a numeric string and enforce the age range |
| `#[model(default)]` on `Vec<String>` | Fill an omitted list with an empty vector |
| `Option<String>` | Allow an omitted note or JSON `null` |

Coercion is explicit. Removing `coerce` makes the string `"36"` fail integer parsing; the JSON number `36` still works.

## 3. Copy the complete program

Put this source in `src/main.rs`. The same program lives at `crates/rydantic/examples/data_contract.rs` in the repository.

```rust crates/rydantic/examples/data_contract.rs theme={"system"}
//! The complete first-model tutorial. Run with `cargo run -p rydantic --example data_contract`.
use rydantic::{Model, SchemaMode};

#[derive(Debug, Model)]
#[model(extra = "forbid")]
struct Signup {
    #[model(trim, min_length = 2)]
    name: String,
    #[model(trim, lowercase, email)]
    email: String,
    #[model(coerce, ge = 18, le = 120)]
    age: u8,
    #[model(default)]
    tags: Vec<String>,
    note: Option<String>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let signup = Signup::parse_json(r#"{"name":" Ada ","email":" ADA@EXAMPLE.COM ","age":"36"}"#)?;
    assert_eq!(signup.name, "Ada");
    assert_eq!(signup.email, "ada@example.com");
    assert_eq!(signup.age, 36);
    assert!(signup.tags.is_empty());
    assert!(signup.note.is_none());
    println!("Accepted: {}", signup.dump_json()?);

    let errors = Signup::parse_json(r#"{"name":"A","email":"invalid","age":17,"unexpected":true}"#)
        .expect_err("all four invalid fields must be reported");
    assert_eq!(errors.len(), 4);
    for error in errors.errors() {
        println!("Rejected {}: {}", error.pointer(), error.code);
    }

    let schema = Signup::json_schema(SchemaMode::Input);
    println!("Input schema generated: {}", schema["$schema"]);
    Ok(())
}
```

## 4. Run both paths

```sh theme={"system"}
cargo run
```

From the repository checkout, run the equivalent example:

```sh theme={"system"}
cargo run --locked -p rydantic --example data_contract
```

The accepted value has a trimmed name, lowercase email, integer age, empty tags and a null note. The next input produces four field errors:

```text theme={"system"}
Accepted: {"age":36,"email":"ada@example.com","name":"Ada","note":null,"tags":[]}
Rejected /name: too_short
Rejected /email: email
Rejected /age: greater_than_equal
Rejected /unexpected: extra_forbidden
```

Use each error's `loc` or `pointer()` to associate it with a form field. `code` is the machine-readable category; `message` is a human-readable explanation. `errors.to_json()` exposes the structured errors for an application response.

## 5. Share the schema

`Signup::json_schema(SchemaMode::Input)` describes accepted input, including local-rule annotations for transformations. `SchemaMode::Output` describes the normalized serialized value. Both are JSON values that can be saved or passed to API tooling.

Keep parsing at the application boundary: the generated schema describes structure, while the model runs trimming, coercion and its other runtime rules. See [JSON Schema](/models/schema) for portability details.

## Try a change

Add `"tags":["rust","api"]` to the accepted input. Then change the minimum age to `21` and submit age `20`. Compare the accepted output with the field path and constraint code in the rejected case.

> **[Next: build a CRUD API](/tutorials/http-api)**
>
> Use the same approach at an HTTP and database boundary.

> **[More model examples](/examples/models)**
>
> Explore enums, borrowed values and streaming JSON.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.