[Canonical page](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/validation/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Validation

Selects how a [`Project`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md>) treats invalid [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/project-data.md>): reject it with an error, or coerce it into valid data the way dhtmlxGantt's import does. It's set through the `validation` field of [`ProjectOptions`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project-options.md>) and applies to the data passed to [`Project::solve`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) and [`Project::analyze`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#analyze>).

## Declaration

```rust
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Validation {
    #[default]
    Strict,
    Repair,
}
```

## Variants

| Variant | Description |
|---|---|
| `Strict` | Default. Invalid input is rejected with [`Error::Input`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>). The rules are listed on [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/project-data.md>). |
| `Repair` | Fixes the input listed below before scheduling and reports each change as a [`Note::Repaired`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/note.md>) in the result's `notes`. Input it can't fix is still rejected with `Error::Input`. |

## What Repair changes

Each row is one fix. The note's `path` points at the field that changed, indexed by its position in the input.

| Input | Fix | Note `path` |
|---|---|---|
| Duplicate id in `tasks`, `links`, `resources`, `assignments` or `calendars` | The last record wins and takes the position of the first one | `tasks[i].id`, `links[i].id`, ... |
| Invalid `pauses` on a task | The task's pause list is cleared | `tasks[i].pauses` |
| Dated constraint without `constraint_date` | `constraint_type` is cleared | `tasks[i].constraint_type` |
| Task with neither start nor end | `start_date` is set to `config.project_start`, or to the earliest date of any task when no project start is set | `tasks[i].start_date` |
| Task (not a milestone) with no duration and only one of its dates | `duration` is set to `1` | `tasks[i].duration` |
| Start, end and duration that don't agree | The dates win: the duration becomes the working time between them. When no duration from the start lands on the end (an end in non-working time, or dates computed on other calendars), the duration is dropped and the task is resolved from its dates | `tasks[i].duration` |
| End date inside a work gap of a task with inconsistent dates | The end moves to the start of the gap | `tasks[i].end_date` |
| Summary with only one of its dates | The missing date is computed from the other date and the duration | `tasks[i].start_date` or `tasks[i].end_date` |
| Assignment `offset` that isn't finite or is negative | `offset` is cleared | `assignments[i].offset` |
| Assignment `duration` that isn't finite or isn't positive | `duration` is cleared | `assignments[i].duration` |

Unscheduled tasks and summaries without dates are left without dates. Repair never falls back to the current date: when an undated task needs a start and the project has neither `config.project_start` nor any dated task, the call fails with [`Error::Input`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>) at `tasks[i].start_date`.

## Example

```rust
use plen_core::{
    DateTime, Note, Project, ProjectData, ProjectOptions, SolveOptions, TaskData, Validation,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let monday = DateTime::from_ymd_hm(2026, 1, 5, 8, 0).ok_or("invalid date")?;

    let mut design = TaskData::new("design");
    design.start_date = Some(monday);
    design.duration = Some(2.0);

    // Same id as the first task: this record replaces it.
    let mut duplicate = TaskData::new("design");
    duplicate.start_date = Some(monday);
    duplicate.duration = Some(3.0);

    // No dates: the start defaults to the earliest task date.
    let mut build = TaskData::new("build");
    build.duration = Some(2.0);

    let data = ProjectData {
        tasks: vec![design, duplicate, build],
        ..ProjectData::default()
    };

    let mut project = Project::new(ProjectOptions {
        validation: Validation::Repair,
    });
    let result = project.solve(Some(data), &SolveOptions::default())?;

    for note in &result.notes {
        if let Note::Repaired { path, message } = note {
            println!("{path}: {message}");
        }
    }
    Ok(())
}
```

## Related

- [`ProjectOptions`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project-options.md>) - where the mode is set
- [`InputError`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/input-error.md>) - what `Strict` returns for invalid input
- [`Note`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/note.md>) - the `Repaired` notes that `Repair` emits
- [Accept messy input](<https://docs.dhtmlx.com/planning-engine/rust/how-to/accept-messy-input.md>) - using `Repair` on imported data
- [Error handling](<https://docs.dhtmlx.com/planning-engine/rust/error-handling.md#repair-instead-of-reject>) - choosing between rejecting and repairing
