StructDefs and Structs
A StructDef is a user-definable schema for structured objects in LittleHorse. StructDefs supersede the capabilities of JSON_OBJ, ensuring that your structured objects match a specific schema before they enter your workflows.
Concepts
A Struct is a complex data type represented as a map of fields and values, while a StructDef is a metadata object that defines the structure and types of a Struct. These concepts allow for strong typing in workflows, replacing the less structured JSON_OBJ and JSON_ARR types over time.
- A
StructDefserves as a blueprint for theStruct, specifying the types and constraints of its fields. - A
Structinstance that holds the actual data at runtime, similar to aJSON_OBJ.
Before you can use a Struct, you must first define a StructDef.
The StructDef
In LittleHorse, a StructDef is a Metadata Object defining the blueprint for an object's schema.
StructDefs define a list of fields that a matching Struct must include. Each StructDef field has a TypeDefinition and may optionally include a default value. StructDefs in LittleHorse are similar to a POJO in Java, a Struct in Go, and a Dataclass in Python.
The Struct
A Struct instance is a type of VariableValue that conforms to a specific StructDef.
When a Struct is passed into the server, it will be compared to the StructDef defined at that value's entrypoint. For example, a Struct passed as an input variable to a WfRun will be compared to the corresponding VariableDef's TypeDefinition.
During this validation process, the server will ensure that each field defined in the StructDef exists in the Struct and that every field's value matches the corresponding TypeDefinition for that field.
You can pass a Struct anywhere you can traditionally pass VariableValues, such as an input variable on a WfRun, an argument to a TaskRun, and the content of an ExternalEvent.
In Practice
To use a Struct in LittleHorse, you need to do the following:
- Define and register the
StructDef - Use the
StructDefin aWfSpec,TaskDef, orExternalEventDef. - Pass a matching
Structinto the correspondingWfRun,TaskRun, orExternalEvent.
In the following example, we will create StructDef defining a Car. Then, we will use that StructDef as an input variable to a WfSpec.
Define the StructDef
Let's define a StructDef representing a Car.
- Java
To define a StructDef in Java, we can define a Java class with the @LHStructDef annotation. Any field with matching Getters and Setters will be serialized into your StructDef schema.
package io.littlehorse.examples;
import io.littlehorse.sdk.worker.LHStructDef;
@LHStructDef(name="car")
public class Car {
private String make;
private String model;
private int year;
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
public Car() {}
public String getMake() {
return this.make;
}
public void setMake(String make) {
this.make = make;
}
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
public int getYear() {
return this.year;
}
public void setYear(int year) {
this.year = year;
}
}
To register your StructDef in Java, run the following code:
package io.littlehorse.examples;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.wfsdk.internal.structdefutil.LHStructDefType;
public class Main {
public static void main(String[] args) {
LHConfig config = new LHConfig();
// LHStructDefType wraps your class with special logic to validate it and convert it to a StructDef
LHStructDefType lhStructDefType = new LHStructDefType(Car.class);
// Calls `RPC PutStructDef` with your StructDef signature
config.getBlockingStub().putStructDef(lhStructDefType.toPutStructDefRequest());
}
}
Use the StructDef in a TaskDef
Now that our StructDef is registered, we can use it in a TaskDef as a parameter for our task.
If you aren't familiar with TaskDefs and how to register them, read the Workflows concepts page before continuing.
- Java
Using a StructDef as a parameter for your task is simple. Just reference the StructDef class in your Task Signature as you would with any other type.
package io.littlehorse.examples;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.worker.LHTaskMethod;
import io.littlehorse.sdk.worker.LHTaskWorker;
class CarTaskWorker {
@LHTaskMethod("describe-car")
public String describeCar(Car car) {
return "You drive a " + car.getBrand() + " " + car.getModel();
}
}
public class RegisterTaskDef {
public static void main(String[] args) {
LHConfig config = new LHConfig();
CarTaskWorker myTaskWorker = new CarTaskWorker();
// Create a Task Worker
LHTaskWorker worker = new LHTaskWorker(myTaskWorker, "describe-car", config);
Runtime.getRuntime().addShutdownHook(new Thread(worker::close));
// Register the TaskDef
worker.registerTaskDef();
// Start the Worker
worker.start();
}
}
Use the StructDef in a WfSpec
Now that our StructDef is registered, we can use it in a WfSpec for defining an input variable.
If you aren't familiar with WfSpecs and how to register them, read the Workflows concepts page before continuing.
- Java
In the following WfSpec, we will define a Struct variable input-car that depends on our StructDef car's schema. We will then pass the input-car object into our describe-car task.
package io.littlehorse.quickstart;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.wfsdk.WfRunVariable;
import io.littlehorse.sdk.wfsdk.Workflow;
public class RegisterWorkflow {
public static final String WF_NAME = "quickstart";
public static void main(String[] args) {
LHConfig config = new LHConfig();
Workflow workflowGenerator = Workflow.newWorkflow(WF_NAME, wf -> {
// We will pass our StructDef class into the `WorkflowThread#declareStruct()` method
WfRunVariable inputCar = wf.declareStruct("input-car", Car.class).required();
wf.execute("describe-car", inputCar);
});
workflowGenerator.registerWfSpec(config.getBlockingStub());
}
}
Run the WfSpec
Finally, we will execute a WfRun and pass in a Struct car as an input variable.
- Java
In Java, we can convert an instance of our StructDef class into a LittleHorse VariableValue Struct using the LHLibUtil.objToVarVal() method.
Once we've converted our Java object into a VariableValue Struct, we can pass it into a RunWfRequest call.
package io.littlehorse.examples;
import io.littlehorse.sdk.common.LHLibUtil;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.common.proto.RunWfRequest;
import io.littlehorse.sdk.common.proto.VariableValue;
public class RunWorkflow {
public static void main(String[] args) {
LHConfig config = new LHConfig();
// Create an instance of our Car
Car inputCar = new Car("Pontiac", "Aztek", 2005);
// Convert it to a LittleHorse VariableValue
// (this builds the Struct)
VariableValue inputCarValue = LHLibUtil.objToVarVal(inputCar);
// Run the workflow
config.getBlockingStub().runWf(RunWfRequest.newBuilder()
.setWfSpecName("quickstart")
.putVariables("input-car", inputCarValue)
.build()
);
}
}
Accessing Struct fields in a WfSpec
You can use the .get() method to access specific fields of a Struct variable in your WfSpecs:
WfRunVariable inputCar = wf.declareStruct("input-car", Car.class).required();
wf.execute("describe-car-brand", inputCar.get("brand"));
Type-Safety Guarantees
So far, we've seen a happy path scenario for how StructDefs can be used to define a blueprint for your complex data objects. But the real magic in StructDefs lies in the compile-time validations they provide and the errors they'll throw if your Struct doesn't match the expected StructDef.
When your StructDef class diverges from the StructDef on the server
If your local StructDef class diverges from the StructDef registered on the server—for instance, when a required field is added or removed—the server will catch the schema mismatch and reject your Struct.
This ensures that your schema is consistent amongst all clients interfacing with a server instance.
Calling .get() on a field that doesn't exist
If you try to call the .get() method in your WfSpec for a field that doesn't exist, the server will reject your PutWfSpec request instantly. No more guessing if a field will exist at runtime—the server guarantees that any Struct entering your workflow will match the corresponding StructDefs schema.
This is a major advantage that Structs have over our primitive type JSON_OBJ. JSON_OBJs have no schema, so you can't predict their structure when designing a WfSpec. In practice, this opens you up to runtime errors when a JSON_OBJ is malformed.
Further Resources
Congrats on learning how to use StructDefs in LittleHorse! This feature introduces a robust way to handle structured data in LittleHorse, enhancing type safety and usability. Future work will focus on improving interoperability with existing data types (like casting JSON_OBJs to Structs) and extending support across all SDKs.
Check out the following resources to keep learning about StructDefs and see other examples of how they can be used: