[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/how-to/exchange-json-with-a-client/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Exchange JSON with a client

Run the scheduling backend for a browser Gantt: take project data and change sets as JSON, schedule them, and send the results back as JSON.

## Prerequisites

- The `org.dhtmlx:plen` dependency, as shown in [Quick start](<https://docs.dhtmlx.com/planning-engine/java/quick-start.md>). The JSON API ships in the same JAR as the typed API, in package `org.dhtmlx.plen`. It needs no JSON library.
- JSON in the Planning Engine format. Task, link and config fields are snake_case (`start_date`, `use_calendars`). Call options and result fields are camelCase (`includeUnchanged`, `baselineId`). Dates go in as local strings (`"2026-01-05"`, `"2026-01-05 08:00"`, `T` separator and seconds allowed) and come back as `"YYYY-MM-DD HH:mm"`.

## Steps

1. Create one [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md>) per open document. The constructor takes the project options as JSON. Pass `"{}"` (or call `new JsonProject()`) for strict validation, or `{"validation":"repair"}` to fix messy data instead of rejecting it:

   ```java
   var project = new JsonProject("{\"validation\":\"repair\"}");
   ```

   The constructor is the only member that throws on bad input. If the options don't decode, it throws [`JsonProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project-exception.md>). [`getErrorJson()`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project-exception.md#geterrorjson>) returns the error envelope, and [`getError()`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project-exception.md#geterror>) returns the [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>).

2. When the client loads a document, pass the project data JSON to [`solve`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md#solve>) and send the returned string back unchanged. `includeUnchanged` makes the result list every task, not only the ones that moved:

   ```java
   String envelope = project.solve(dataJson, "{\"includeUnchanged\":true}");
   ```

3. While the user drags or edits, pass the change set to [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md#preview>). It schedules the change without keeping it:

   ```java
   String envelope = project.preview("{\"tasks\":{\"update\":[{\"id\":\"a\",\"duration\":3}]}}", "{}");
   ```

4. When the user commits, pass the same change set to [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md#apply>), then call `solve` with `null` data to reschedule the accepted state:

   ```java
   String report = project.apply(changesJson);
   String schedule = project.solve(null, "{}");
   ```

   Only the data argument of `solve` and `analyze` can be `null`. Pass `"{}"` when you have no options: a `null` options string throws `NullPointerException`.

5. Lock the project around every call. A `JsonProject` isn't synchronized, and an HTTP server with a thread pool can run two requests for the same document at once. See [Share a project between threads](<https://docs.dhtmlx.com/planning-engine/java/how-to/share-a-project-between-threads.md>).

6. On the client, check `ok` in every response and read `result` or `error`, as described in [Read the envelope](<https://docs.dhtmlx.com/planning-engine/java/how-to/exchange-json-with-a-client.md#read-the-envelope>).

## Full example

A minimal server on the JDK's built-in `com.sun.net.httpserver` with three endpoints: load, preview and commit. Each one reads the request body as JSON text and returns the envelope as `application/json`:

```java
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import org.dhtmlx.plen.JsonProject;

public class GanttServer {
  // One project per open document.
  static final ConcurrentHashMap<String, JsonProject> projects = new ConcurrentHashMap<>();

  public static void main(String[] args) throws Exception {
    var server = HttpServer.create(new InetSocketAddress(8080), 0);
    server.createContext("/projects/", GanttServer::handle);
    server.setExecutor(Executors.newFixedThreadPool(8));
    server.start();
  }

  // POST /projects/{id}/load | preview | apply
  static void handle(HttpExchange exchange) throws IOException {
    String[] parts = exchange.getRequestURI().getPath().split("/");
    if (!"POST".equals(exchange.getRequestMethod()) || parts.length != 4) {
      send(exchange, 404, "");
      return;
    }
    String id = parts[2];
    String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);

    if (parts[3].equals("load")) {
      // Load: schedule the full data and return every task's dates.
      var project =
          projects.computeIfAbsent(id, key -> new JsonProject("{\"validation\":\"repair\"}"));
      synchronized (project) {
        send(exchange, 200, project.solve(body, "{\"includeUnchanged\":true}"));
      }
      return;
    }

    var project = projects.get(id);
    if (project == null) {
      send(exchange, 404, "");
      return;
    }
    switch (parts[3]) {
      // Preview: schedule a change set without keeping it.
      case "preview" -> {
        synchronized (project) {
          send(exchange, 200, project.preview(body, "{\"reasons\":true}"));
        }
      }
      // Commit: apply a change set, reschedule, return both envelopes.
      case "apply" -> {
        String response;
        synchronized (project) {
          String report = project.apply(body);
          String schedule = project.solve(null, "{}");
          response = "{\"apply\":" + report + ",\"schedule\":" + schedule + "}";
        }
        send(exchange, 200, response);
      }
      default -> send(exchange, 404, "");
    }
  }

  static void send(HttpExchange exchange, int status, String json) throws IOException {
    byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
    exchange.getResponseHeaders().set("Content-Type", "application/json");
    exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 : bytes.length);
    try (var out = exchange.getResponseBody()) {
      out.write(bytes);
    }
  }
}
```

The same calls fit a Spring or Jakarta EE controller: take the request body as a `String`, call the `JsonProject` method inside the lock, and return the envelope string with the `application/json` content type.

Every call is synchronous and CPU-bound. For large projects, or for `level`, `evaluate` and `suggestAssignments`, keep the work off threads that must stay responsive. See [Call from async code](<https://docs.dhtmlx.com/planning-engine/java/how-to/call-from-async-code.md>).

## Read the envelope

Every [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md>) method returns one of two shapes. Keys are sorted at every depth, and whole numbers are written as integers.

Success:

```json
{"ok":true,"result":{"baselineId":"1-b0f04113e2b02bf4","conflicts":[],"converged":true,"cycles":[],"notes":[],"updates":[...]}}
```

Failure:

```json
{"error":{"code":null,"kind":"validation","message":"task needs two of start_date / end_date / duration","path":"tasks[0].start_date"},"ok":false}
```

Engine errors never escape as exceptions. Read `error.kind` to decide what to do:

| `kind` | Cause | `path` | `code` |
|---|---|---|---|
| `"parse"` | JSON that doesn't decode: bad syntax, duplicate keys, a wrong type, an unknown option or change-set key | `null` | `null` |
| `"validation"` | Data the engine rejects, such as a task with fewer than two of start, end and duration | The field, e.g. `"tasks[0].start_date"`. `null` when the decoder rejects a value | `null` |
| `"project"` | A call the project's state doesn't allow | `null` | `"no-baseline"`, `"stale-baseline"` or `"needs-resync"` |

Handle the project codes on the client:

- `no-baseline`: the project hasn't been solved yet. Send the full data to the load endpoint first.
- `stale-baseline`: the `baselineId` the client sent in the options is out of date. Reload the document.
- `needs-resync`: an `apply` rejected part of a change set. The apply envelope has `"needsResync":true` and lists the rejections in `rejected`. Until you send the full data to `solve` again, `solve(null, …)`, `apply` and `level` fail with this code. See [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/java/how-to/recover-from-rejected-changes.md>).

Each error kind maps to an exception in the typed API: `"parse"` and `"validation"` to [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>), `"project"` to [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>). See [Error handling](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md>).

Unknown fields in task, link, resource and assignment records are ignored, so you can send records with their extra UI properties (`text`, `progress`, custom fields). Call options and the change set's own keys are strict: an unknown key there fails with `"parse"`.

## The JsonProject methods

Each method takes JSON text and returns the envelope string:

| Method | Input | Result |
|---|---|---|
| `solve(data, options)` | Project data, or `null` to re-solve the accepted data | Schedule result |
| `preview(changes, options)` | Change set | Schedule result, not accepted |
| `analyze(data, options)` | Project data, or `null` | Schedule result with analytics, not accepted |
| `apply(changes)` | Change set | Apply report |
| `level(options)` | Leveling options | Leveling result, accepted |
| `loadProfile(options)` | Load-profile query | Load profile |
| `evaluate(candidates, options)` | Candidate list | Evaluate result |
| `slots(pool, requirements)` | Worker pool and requirement list | Slot list |
| `suggestAssignments(slots, options)` | Slot list | Suggest result |

[`copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md#copy>) returns an independent `JsonProject`. Use it to preview a leveling run or a multi-step edit before you accept it. The fields of each input and result are the wire names of the typed records. See [Reading results](<https://docs.dhtmlx.com/planning-engine/java/scheduling/reading-results.md>) and the [API reference](<https://docs.dhtmlx.com/planning-engine/java/api/overview.md>).

## Schedule once, without a project

For a stateless endpoint, call [`Wire.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md>). It decodes the data and options, schedules them in a fresh project and returns the envelope:

```java
import org.dhtmlx.plen.Wire;

String envelope = Wire.schedule(dataJson, "{}");
```

`Wire.schedule` always uses strict validation.

## Decode JSON into typed records

To keep working with the typed [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>), decode the JSON with [`Decode`](<https://docs.dhtmlx.com/planning-engine/java/api/json/decode.md>) and wrap each call with [`Wire.envelopeText`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md#envelopetext>):

```java
import java.util.Map;
import org.dhtmlx.plen.*;

ProjectData data = Decode.fromJson(dataJson, ProjectData.class);
SolveOptions options = Decode.fromJson("{\"includeUnchanged\":true}", SolveOptions.class);

var project = new Project();
String envelope = Wire.envelopeText(() -> project.solve(data, options));

// A change set that is already parsed, e.g. part of a larger request body.
var body = (Map<?, ?>) Json.parse(requestJson);
SchedulingChanges changes = Decode.changes(body.get("changes"));
String report = Wire.envelopeText(() -> project.apply(changes));
```

- `Decode.fromJson(text, type)` decodes [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/project-data.md>), [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md>), every options type, and the records nested in them. [`Decode.list(text, type)`](<https://docs.dhtmlx.com/planning-engine/java/api/json/decode.md#list>) decodes a JSON array of records, such as candidates, slots or requirements.
- `Decode.project`, `Decode.changes`, `Decode.solveOptions` and the other named decoders take a tree you've already parsed with [`Json.parse`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json.md>). [`Decode.fromValue(value, type)`](<https://docs.dhtmlx.com/planning-engine/java/api/json/decode.md#fromvalue>) does the same for any record type.
- A decode failure throws [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) with `kind` `PARSE` (or `VALIDATION` for a value the decoder rejects) and a `null` `path`.
- `Wire.envelopeText(run)` runs the call and returns `{"ok":true,"result":…}`, or the error envelope if `run` throws a [`PlenException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/plen-exception.md>). Any other exception propagates. Decode inside `run` to get decode errors as envelopes too.
- [`Wire.errorText(exception)`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md#errortext>) builds the error envelope for an exception you caught yourself, and [`Wire.resultText(value)`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md#resulttext>) builds the success envelope for a typed result.
- [`Encode.text(result)`](<https://docs.dhtmlx.com/planning-engine/java/api/json/encode.md>) returns the bare result JSON, without the envelope, for logging or for a response format of your own.

To mix typed and JSON calls on one project, create it with [`Wire.newProject(optionsJson)`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md#newproject>) and pass it to `Wire.solve(project, …)`, `Wire.apply(project, …)` and the other static `Wire` methods. They return the same envelopes as the `JsonProject` methods.

[`Wire.envelope`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md#envelope>) and [`Wire.error`](<https://docs.dhtmlx.com/planning-engine/java/api/json/wire.md#error>) return the envelopes as a parsed tree: `Map<String, Object>`, `List<Object>`, `String`, `Boolean`, `null`, and numbers as `Long` or `Double`. Read numbers as `Number`. To send an envelope, send the `envelopeText` / `errorText` string.

## Result

The load endpoint answers with every task's dates:

```json
{"ok":true,"result":{"baselineId":"1-b0f04113e2b02bf4","conflicts":[],"converged":true,"cycles":[],"notes":[],"updates":[{"duration":2,"end_date":"2026-01-07 08:00","id":"a","kind":"task","start_date":"2026-01-05 08:00"},{"duration":1,"end_date":"2026-01-08 08:00","id":"b","kind":"task","start_date":"2026-01-07 08:00"}]}}
```

That's the response for two tasks `a` (2 days) and `b` (1 day) linked finish-to-start, both starting on `2026-01-05 08:00`. Each `updates` entry uses the task-data field names, so the client can write it straight back to its gantt task. The `baselineId` differs on every run.
