Accept messy input#

Schedule project data that strict validation rejects, such as data with duplicate ids or missing dates, and get a list of every fix the engine made.

By default a Project uses Validation.STRICT and throws InputException for the first invalid field. Validation.REPAIR coerces the data into valid input instead, then schedules the result.

Steps#

  1. Create the project with repair validation:

    var project = new Project(new ProjectOptions().withValidation(Validation.REPAIR));
    
  2. Pass your data to Project#solve as usual. Project#analyze with non-null data repairs it the same way. Engine.schedule always uses strict validation, so use a Project here.

  3. Read the Note.Repaired entries in result.notes and show them to the user, or log them. Each one has a path and a message.

The full example below has four kinds of problems: a duplicate id, a task with no dates, a dated constraint with no date, and a task whose start, end and duration disagree. Strict validation rejects this data. Repair schedules it:

import java.util.List;
import org.dhtmlx.plen.*;

public class AcceptMessyInput {
  public static void main(String[] args) throws Exception {
    var jan1 = Moment.fromYmdHm(2026, 1, 1, 0, 0);
    var jan4 = Moment.fromYmdHm(2026, 1, 4, 0, 0);

    // Start and end without a duration: valid, nothing to repair.
    var design = new TaskData(Id.str("design")).withStartDate(jan1).withEndDate(jan4);

    // "build" appears twice. The later record wins.
    var buildOld = new TaskData(Id.str("build")).withStartDate(jan1).withDuration(1.0);
    var build = new TaskData(Id.str("build")).withStartDate(jan1).withDuration(2.0);

    // No dates, no duration, and a dated constraint without a constraint date.
    var review = new TaskData(Id.str("review")).withConstraintType(ConstraintType.SNET);

    // Three days between start and end, but the duration says five.
    var deploy = new TaskData(Id.str("deploy"))
        .withStartDate(jan1)
        .withEndDate(jan4)
        .withDuration(5.0);

    var data = new ProjectData()
        .withTasks(List.of(design, buildOld, review, build, deploy));
    // 24/7 time keeps the example dates easy to check.
    data.config.useCalendars = false;

    var project = new Project(new ProjectOptions().withValidation(Validation.REPAIR));
    var options = new SolveOptions().withIncludeUnchanged(true);
    var result = project.solve(data, options);

    for (var note : result.notes) {
      if (note instanceof Note.Repaired r) {
        System.out.println(r.path + ": " + r.message);
      }
    }
    for (var m : result.updates) {
      System.out.println(m.id + " " + m.startDate + ".." + m.endDate + " " + m.duration);
    }
  }
}

What repair changes#

Repair reports each change below as one Note.Repaired, with the field path in path:

  • Duplicate ids in tasks, links, resources, assignments and calendars: the last record replaces the earlier one and takes its position. The note is at tasks[i].id, where i is the index of the duplicate in your input.
  • A task with no start and no end gets startDate set to config.projectStart, or to the earliest date of any task if projectStart isn't set.
  • A leaf task (not a milestone) with no duration and only one date gets duration 1.
  • Start, end and duration that disagree: the dates win, and the duration becomes the working time between them. If the end is in non-working time, or the dates were computed on a different calendar, repair drops the duration and resolves the task from its dates. An end that falls inside a pause moves to the pause start, with a second note at tasks[i].end_date.
  • A summary with only one date gets the other date computed from that date and its duration.
  • A dated constraint (SNET, SNLT, FNET, FNLT, MSO, MFO) without constraintDate is cleared.
  • Invalid pauses on a task: the whole list is cleared, with a note at tasks[i].pauses.
  • An invalid assignment offset or duration is cleared, so the assignment books the whole task.

A task that already has two of start, end and duration is valid input. Repair doesn't touch it and doesn't report it.

Note paths use the JSON field names (start_date, constraint_type), not the Java field names.

Only the duplicate-id notes use input indexes. Every other note indexes the deduplicated list. In the example, the two build records become one, so deploy is tasks[4] in your input but tasks[3] in its note.

Repair works on the project's own copy of the data. The ProjectData you pass in isn't changed.

What repair doesn't fix#

  • An entirely undated project, with no config.projectStart and no task that has a date, still throws InputException at tasks[i].start_date. The engine never falls back to today's date.
  • Every other invalid input, such as a cycle in the parent hierarchy or a calendar with no working time, still throws InputException. See Error handling.
  • If repair or validation fails, solve throws and the project keeps its previous state.

Result#

The example prints the fixes, then the scheduled dates (the date lines may come in a different order):

tasks[3].id: duplicate id replaced the earlier record, retaining its position
tasks[2].constraint_type: dated constraint without date cleared
tasks[2].start_date: missing date defaulted to project anchor
tasks[2].duration: missing duration defaulted to one
tasks[3].duration: duration recomputed from supplied start and end
design 2026-01-01 00:00..2026-01-04 00:00 3.0
build 2026-01-01 00:00..2026-01-03 00:00 2.0
review 2026-01-01 00:00..2026-01-02 00:00 1.0
deploy 2026-01-01 00:00..2026-01-04 00:00 3.0

build uses the second record's duration of 2, and deploy has a duration of 3, taken from its dates. With new Project(), the same solve call throws InputException.