REST API
The REST API module will automatically generate REST API endpoints so you can interact with the optimization service.
| This page describes features which are only relevant when running Timefold Solver as a Service (Preview). The information on these pages may describe functionality which may be changed or even removed in a future release. |
1. Setup
In order for the API to be automatically generated, Timefold Solver will look for implementations of a couple interfaces. The following are expected.
-
An implementation of
ModelInputto define the class which will be mapped from JSON to Java on API requests. -
An implementation of
ModelOutputto define the class which will be mapped from Java to JSON on API responses. -
An interface which extends
ModelRestto define the root path of the REST API.
For example, in the case of School Timetabling, those classes could look as follows.
// TimetableDto.java
public class TimetableDto implements ModelInput, ModelOutput {
@Schema(required = true, description = "The unique identifier of timetable")
private String name;
@Schema(required = true, description = "List of timeslots")
private List<Timeslot> timeslots;
@Schema(required = true, description = "List of lessons")
private List<Lesson> lessons;
// Getters, Setters and constructor excluded.
}
// TimetableSchedulingResource.java
@Tag(name = "School Timetabling",
description = "School timetabling service assigning lessons to timeslots.") // OpenAPI documentation annotation
@Path("/v1/timetables") //sets the root path
public interface TimetableSchedulingResource extends ModelRest {
}
Note how the ModelInput and ModelOutput interface can be placed on the same class.
Since an OpenAPI specification will also be automatically generated, it’s important to properly document your ModelInput and ModelOutput classes with OpenAPI annotations.
2. Generated endpoints
The following endpoints are generated by the REST module.
The <root> is determined by the @Path annotation on the ModelRest interface (/v1/timetables in the example above).
-
GET /<root>: Retrieve all registered optimization datasets -
POST /<root>: Create a new optimization dataset -
GET /<root>/{id}: Retrieve the (intermediate) result of a dataset optimization run -
GET /<root>/{id}/score-analysis: Retrieve score analysis of an optimization dataset -
DELETE /<root>/{id}: Terminate a dataset optimization run
If your model provides demo data, the following endpoints are also created.
-
GET /<root>/demo-data: Retrieve all available demo dataset names. -
GET /<root>/demo-data/{name}: Retrieve the demo dataset with the given identifier
3. Request and response structure
The generated endpoints do not accept/return the pure ModelInput/ModelOutput objects as JSON.
Instead, a fixed envelop is used for both the requests and responses of solver actions.
3.1. Structured solving request
A Solving Request always contains at least 2 major parts, the modelInput object and a config object.
-
The
modelInputobject contains the JSON formattedModelInputobject defined in Java. -
The
configobject has two fields: the run and the model.
Additionally, more fields might be added for specific model implementations.
{
"config": {
"run": {
"name": "dataset name",
"termination": {
"spentLimit": "PT5M",
"unimprovedSpentLimit": "PT10S"
},
"maxThreadCount": 1,
"tags": []
},
"model": {
"overrides": "<model-specific configurations>"
}
},
"modelInput" : "<ModelInput class as JSON>"
}
The run configuration has four fields:
-
name- The run name (if empty, it will be generated). -
maxThreadCount- The maximum thread count, which indicates the maximum number of threads to be used for solving. If not provided, 1 will be used. -
tags- The tags, which are a set of optional tags to be assigned to the run. -
termination- The termination properties determining how long the solver should run:-
spentLimit- The maximum duration to keep the solver running (ISO 8601 Duration). If omitted, no maximum duration will be set (platform limits will apply). -
unimprovedSpentLimit- The maximum unimproved score duration, i.e. if the score has not improved during this period, the solver will terminate (ISO 8601 Duration). If omitted, the diminished returns termination will be used.
-
The diminished returns termination is the recommended default setting.
This termination is desirable since it terminates based on the relative rate of improvement, and behaves similarly on different hardware and different problem instances.
unimprovedSpentLimit should be set only when necessary.
|
The model configuration is a model-specific field, that contains additional global model configuration attributes.
See Model Configuration Overrides for more information.
3.2. Structured solving response
A Solving Response always contains at least 2 major elements, the modelOutput object and a metadata object.
-
The
modelOutputobject contains the JSON formattedModelOutputobject defined in Java. -
The
metadataobject provides more details about the optimization run.
Additionally, more fields might be added for specific model implementations:
-
inputMetrics: metrics about the input of the planning problem. See: Input Metrics -
kpis: metrics about the output of the planning problem. See: Output Metrics
{
"metadata": {
"id": "dataset-id",
"name": "dataset-name",
"submitDateTime": "2022-03-10T12:15:50-04:00",
"startDateTime": "2022-03-10T12:15:50-04:00",
"activeDateTime": "2022-03-10T12:15:50-04:00",
"completeDateTime": "2022-03-10T12:15:50-04:00",
"shutdownDateTime": "2022-03-10T12:15:50-04:00",
"solverStatus": "NOT_SOLVING",
"score": "string",
"tags": [],
"validationResult": {
"summary": "VALIDATION_NOT_SUPPORTED",
"errors": [],
"warnings": []
}
},
"modelOutput": <ModelOutput class as JSON>,
"inputMetrics": {
"lessons": 200,
"timeslots": 50
},
"kpis": {
"unassignedLessons": 0,
"maxConsecutiveLessons": 3,
"earliestTimeslotStart": "2027-02-02T09:00:00Z",
"latestTimeslotStart": "2027-02-05T17:00:00Z"
}
}
The metadata object contains a couple of fields:
-
id- The dataset id, as generated by the Solver. -
name- The dataset name. -
solverStatus- The status of the optimization. -
score- The score of the solution. -
tags- The assigned tags of the dataset. -
validationResult- The result of the validation. This might signal errors in the original input.
The remaining fields are timestamps for each step in the optimization lifecycle.
The metadata object contains five key timestamps, each marking a distinct phase in the process:
-
submitDateTime: The moment the dataset is submitted. At this stage, the dataset is queued and has not yet begun solving. The dataset status isSOLVING_SCHEDULED. -
startDateTime: The moment the run begins initializing. During this phase, the dataset is not solving yet, but the model is being built and enriched with external data (e.g., incorporating map information for models involving geographical locations). The dataset status isSOLVING_STARTED. -
activeDateTime: The moment the solving phase begins. At this stage, the actual problem-solving process starts. The dataset status isSOLVING_ACTIVE. -
completeDateTime: The moment the solving phase concludes. At this point, the dataset has finished solving but has not yet undergone any post-processing (e.g., score analysis or waypoint enrichment for geographical models). The dataset status is eitherSOLVING_COMPLETEDorSOLVING_FAILED. -
shutdownDateTime: The moment the post-processing phase finishes. All tasks, including post-processing, are completed, and the dataset optimization is fully finalized.
4. Decoupling API interaction from the solver domain
While it is possible to add the ModelInput and ModelOutput directly on the SolverModel (the @PlanningSolution class used by the solver),
it’s often beneficial to decouple the classes used for the REST API interactions and the classes used for the optimization.
To convert between ModelInput / ModelOutput and the SolverModel, a ModelConvertor implementation can be provided.
@ApplicationScoped
public class TimetableConvertor implements ModelConvertor<HardSoftLongScore, TimetableDto, Timetable, EmptyModelConfigOverrides, EmptyModelInputMetrics, EmptyModelOutputMetrics, Timetable> {
@Override
public Timetable toSolverModel(TimetableDto modelInput, TimetableDto previousModelOutput, ModelConfig<EmptyModelConfigOverrides> modelConfig) {
return // Mapping logic
}
@Override
public Timetable toSolverModel(TimetableDto modelInput, ModelConfig<EmptyModelConfigOverrides> modelConfig) {
return // Mapping logic
}
@Override
public TimetableDto toModelOutput(Timetable solverModel) {
return // Mapping logic
}
}
|
This mechanism should only be used for ModelInput → SolverModel → ModelOutput mapping. For enhancing the SolverModel, use the SolverModel Enrichment mechanism instead. |
5. Validating REST input
By default, validation on the input is based on the OpenAPI Specification annotations.
Additional validations can be added by implementing the ModelValidator interface.
@ApplicationScoped
public class TimetableValidator implements ModelValidator<TimetableDto, TimetableConfigOverrides> {
@Override
public void validate(ValidationBuilder validationBuilder, TimetableDto input, ModelConfig<TimetableConfigOverrides> modelConfig) {
//validation logic here, simplified example here.
if(hasDuplicateTeacherNames(input)) {
validationBuilder.addIssue(TimetableValidationIssue.DUPLICATE_TEACHER.asIssueType(), new DuplicateTeacherDetail("Ann"));
}
}
}
public enum TimetableValidationIssue {
DUPLICATE_TEACHER(IssueCode.of("DUPLICATE_TEACHER"), IssueSeverity.ERROR,
"Duplicate teacher names found.");
private final IssueType issueType;
TimetableValidationIssue(IssueCode code, IssueSeverity severity, String message) {
this.issueType = new IssueType(code, severity, message);
}
public IssueType asIssueType() {
return issueType;
}
public record DuplicateTeacherDetail(String teacherName) implements IssueDetail {
}
}
The ValidationBuilder supports issue types of different severity:
-
IssueSeverity.ERROR: error level: processing can not continue. Results in a BAD_REQUEST (400) HTTP error. -
IssueSeverity.WARNING: warning level: processing will continue.
All validation information can be accessed either in a simple textual form, or as structured machine-readable objects.
5.1. Simple validation result in metadata object
The metadata object is a part of the model response json and contains the validation result in a textual form to provide an immediate feedback on the input json.
{
"metadata": {
"id": "dataset-id",
"name": "dataset-name",
"submitDateTime": "2022-03-10T12:15:50-04:00",
"startDateTime": "2022-03-10T12:15:50-04:00",
"activeDateTime": "2022-03-10T12:15:50-04:00",
"completeDateTime": "2022-03-10T12:15:50-04:00",
"shutdownDateTime": "2022-03-10T12:15:50-04:00",
"solverStatus": "NOT_SOLVING",
"score": "string",
"tags": [],
"validationResult": {
"summary": "ERRORS",
"errors": ["Duplicate teacher names found."],
"warnings": []
}
},
"modelOutput": "<ModelOutput class as JSON>"
}
5.2. Machine-readable validation result
Additionally, a richer validation result can be obtained by a dedicated REST endpoint:
GET /v1/timetables/{id}/validation-result
Assuming the example TimetableValidator, the endpoint provides the following response:
GET /v1/timetables/{id}/validation-result.{
"status" : "ERRORS",
"issues" : [ {
"id" : 1,
"code" : "DUPLICATE_TEACHER",
"severity" : "ERROR",
"detail" : {
"teacherName" : "Ann"
}
} ]
}
The machine-readable validation result enables for corrective actions. In this example, the timetable can be corrected by removing the duplicate teacher Ann.
6. Adding custom endpoints
You can add custom endpoints in the ModelRest implementation.
For example, if you want to extend the Timetable REST API, you can add custom Jakarta REST endpoints.
These endpoints should also be documented appropriately for the automatically generated OpenAPI specification.
@Tag(name = "School Timetabling",
description = "School timetabling service assigning lessons to timeslots.") //OpenAPI documentation annotation
@Path("/v1/timetables") //sets the root path
public interface TimetableSchedulingResource extends ModelRest {
@APIResponses(value = {
@APIResponse(responseCode = "500", description = "In case of processing errors",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = ErrorInfo.class))),
@APIResponse(responseCode = "200", description = "List of all teachers in all optimization runs.",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = ListTeachersResponseDto.class))) })
@Operation(operationId = "list-teachers-in-solver-model",
summary = "Lists all teachers in the solver model.")
@GET
@Path("/insights/teachers")
@Produces(MediaType.APPLICATION_JSON)
default Response insightsTeachers() {
var teachers = //get the list of teachers
return Response.ok(teachers).build();
}
}
7. OpenAPI specification
The OpenAPI specification is a machine-readable document that defines the structure, endpoints, request/response formats, and authentication of a RESTful API. A specification file is automatically generated from the MicroProfile OpenAPI annotations on the Java classes.
| For more info on which annotations are available and how they relate to the generated OpenAPI specification, check out the official MicroProfile OpenAPI specification here. |
After building the model with mvn verify, the generated API spec can be found in the target folder: target/openapi-schema/openapi.json.
7.1. Deliberate API changes
As with any API, breaking the REST API in newer versions of your model should be avoided to ensure backward compatibility and prevent disruptions for existing consumers. Because Timefold Solver takes care of a lot of boilerplate code, it might be harder to see when an API change was accidentally introduced.
Timefold Solver supports comparing a pre-existing OpenAPI specification file in your project src/build/openapi.json with the one generated during the build.
If there is any difference between the 2 files, the build will fail.
If the API changes are intentional, run the following command which will copy the generated target/openapi-schema/openapi.json to src/build/openapi.json file.
mvn clean package -Dupdate-api
This approach ensures that any change to the API has to be reflected in the src/build/openapi.json file.
| We recommend to keep this file in Version Control, this makes sure the changes made are deliberate (committed and pushed) rather than accidental. |
7.2. Customize summary of solver methods
As mentioned before, the REST endpoints for solver methods are automatically generated. This means users do not have the option to set the OpenAPI Annotations for those methods.
Timefold Solver does support setting a custom summary through the use of properties in the application.properties file.
This uses the following format:
timefold.rest.<operationname>.summary=Your custom summary here
The <operationname> is the name of the method in the API Spec (operationId field).
For example, if you want to override the OpenAPI summary for the schedule operation, add the following entry in your properties file:
timefold.rest.schedule.summary=Request a timetable to be solved asynchronously.