SDK Type Adapters
Type Adapters allow the LittleHorse SDK to convert between your types and LittleHorse's native typing system.
✅ With Type Adapters, you can use any Java class directly in @LHTaskMethod signatures and @LHStructDef fields. You define how to convert that class to and from a supported LittleHorse variable type, and the SDK handles the rest. This leads to cleaner code and better type safety across your workflows.
❌ Before Type Adapters, task methods and workflow variables were limited to types the SDK natively understood—String, Long, Boolean, byte[], etc... If the SDK didn't recognize a type, it would fallback to JSON serialization/deserialization, which has unexpected results with some types.
We are currently rolling out the Type Adapter feature to all SDKs. The Java SDK has full support for Type Adapters, while other language SDKs will gain support in the coming months.
When to Use Type Adapters
Use Type Adapters when you want to work with types that don't have a direct mapping to a LittleHorse's typing system. Common examples include:
- Value objects:
UUID,Email,Money, or other domain-specific types. - Date/time types:
ZonedDateTime,Period,Duration, etc... - Third-party library types: Any class from an external library that you want to pass through LittleHorse workflows.
- Custom enums: Enumerations that map naturally to a string or integer representation.
Without an adapter, using a UUID as a task method parameter would require manually converting it to and from String in your task code. With an adapter registered, the SDK handles this conversion automatically wherever the type is encountered—in task definitions, task execution, workflow variables, and @LHStructDef fields.
Type Adapter Interfaces
Each LittleHorse VariableType has a corresponding adapter interface. The table below lists all available adapter types:
| Adapter Interface | LH Variable Type | Conversion Methods |
|---|---|---|
LHStringAdapter<T> | STR | toString(T) / fromString(String) |
LHLongAdapter<T> | INT | toLong(T) / fromLong(Long) |
LHIntegerAdapter<T> | INT | toInteger(T) / fromInteger(Integer) |
LHDoubleAdapter<T> | DOUBLE | toDouble(T) / fromDouble(Double) |
LHBooleanAdapter<T> | BOOL | toBoolean(T) / fromBoolean(Boolean) |
LHBytesAdapter<T> | BYTES | toBytes(T) / fromBytes(byte[]) |
LHTimestampAdapter<T> | TIMESTAMP | toTimestamp(T) / fromTimestamp(Timestamp) |
LHJsonObjAdapter<T> | JSON_OBJ | toJsonObj(T) / fromJsonObj(Object) |
LHJsonArrAdapter<T> | JSON_ARR | toJsonArr(T) / fromJsonArr(List) |
LHWfRunIdAdapter<T> | WF_RUN_ID | toWfRunId(T) / fromWfRunId(WfRunId) |
Choose the adapter interface that matches the LittleHorse variable type you want your Java class to map to. For example, if your custom type should be stored as a STR in LittleHorse, implement LHStringAdapter<T>.
Creating a Custom Type Adapter
This section walks through creating and registering a Type Adapter for java.util.UUID, which maps to LittleHorse's STR type.
Step 1: Choose the Adapter Interface
Since a UUID is naturally represented as a string, we use LHStringAdapter<UUID>:
import io.littlehorse.sdk.common.adapter.LHStringAdapter;
import java.util.UUID;
public class UUIDAdapter implements LHStringAdapter<UUID> {
@Override
public String toString(UUID src) {
return src.toString();
}
@Override
public UUID fromString(String src) {
return UUID.fromString(src);
}
@Override
public Class<UUID> getTypeClass() {
return UUID.class;
}
}
Every adapter must implement three things:
getTypeClass()— returns the Java class this adapter handles.- A
tomethod — converts from your Java type to the LH type (here,toString(UUID)). - A
frommethod — converts from the LH type back to your Java type (here,fromString(String)).
Step 2: Register the Adapter
Registration
Register your adapter when building your LHConfig:
LHConfig config = LHConfig.newBuilder()
.loadFromProperties(props)
.addTypeAdapter(new UUIDAdapter())
.build();
LHTypeAdapters registered on LHConfig are available to all workers and workflows that use that config:
- In Task Workers
- In Workflows
- In StructDefs
If you want to use LHTypeAdapters in your LHTaskWorker, you can do so by passing your LHConfig into the worker's constructor:
LHTaskWorker worker = new LHTaskWorker(executable, "task-name", config)
If you want to use LHTypeAdapters in your WfSpec registration methods, you can do so by passing your LHConfig into the registerWfSpec method:
Workflow workflow = new WorkflowImpl("my-workflow", wf -> {
// This will register your ExternalEventDef with the adapted type
wf.waitForEvent("some-event").registeredAs(YourAdaptedClass.class);
});
workflow.registerWfSpec(config);
If you want to use LHTypeAdapters to adapt field types in your @LHStructDef-annotated classes, you can do so by passing your LHConfig's TypeAdapterRegistry into the LHStructDefType constructor:
LHStructDefType structDefType = new LHStructDefType(structDefClass, config.getTypeAdapterRegistry());
PutStructDefRequest request = structDefType.toPutStructDefRequest().toBuilder()
.setAllowedUpdates(StructDefCompatibilityType.FULLY_COMPATIBLE_SCHEMA_UPDATES)
.build();
config.getBlockingStub().putStructDef(request);
Use with Task Methods
With the adapter registered, you can use UUID directly in your @LHTaskMethod signatures:
import io.littlehorse.sdk.worker.LHTaskMethod;
import io.littlehorse.sdk.worker.WorkerContext;
import java.util.UUID;
public class Worker {
@LHTaskMethod(value = "get-uuid", description = "Generates and returns a random UUID.")
public UUID getUUID() {
return UUID.randomUUID();
}
@LHTaskMethod(value = "echo-uuid", description = "Receives a UUID and logs it.")
public void echoUUID(UUID uuid, WorkerContext context) {
context.log("Received UUID via adapter: " + uuid);
}
}
Now, register your Task Workers with the same LHConfig that contains your adapter:
package io.littlehorse.examples;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.wfsdk.WfRunVariable;
import io.littlehorse.sdk.wfsdk.Workflow;
import io.littlehorse.sdk.wfsdk.internal.WorkflowImpl;
import io.littlehorse.sdk.worker.LHTaskWorker;
import java.io.IOException;
public class RunWorkers {
public static void main(String[] args) throws IOException {
LHConfig config = LHConfig.newBuilder()
.addTypeAdapter(new UUIDTypeAdapter())
.build();
Worker executable = new Worker();
LHTaskWorker getUuidWorker = new LHTaskWorker(executable, "get-uuid", config);
LHTaskWorker echoUuidWorker = new LHTaskWorker(executable, "echo-uuid", config);
// Register TaskDefs
getUuidWorker.registerTaskDef();
echoUuidWorker.registerTaskDef();
// Start workers
getUuidWorker.start();
echoUuidWorker.start();
}
}
In the above code, the SDK automatically:
- Registers the
get-uuidTaskDef with a return type ofSTR. - Registers the
echo-uuidTaskDef with one input parameter of typeSTR. - Converts the
UUIDreturn value to aStringwhen sending task output. - Converts the incoming
Stringvalue to aUUIDwhen providing task input.
Now you can pass STR values to your tasks and they will be automatically converted to UUID by the SDK.
Validation
The SDK validates adapter compatibility at worker startup. When an LHTaskWorker starts, it checks that each adapted parameter's VariableType matches the corresponding field in the TaskDef schema. If there is a mismatch, the worker will report a validation error:
TaskDef provides INT, but adapter for java.util.UUID maps to STR
This ensures that your adapters and TaskDef schemas stay in sync.
Writing Adapters for Other Types
Here are a few more examples to illustrate how to write adapters for different scenarios.
An Enum Adapter (String-based)
public class StatusAdapter implements LHStringAdapter<OrderStatus> {
@Override
public String toString(OrderStatus src) {
return src.name();
}
@Override
public OrderStatus fromString(String src) {
return OrderStatus.valueOf(src);
}
@Override
public Class<OrderStatus> getTypeClass() {
return OrderStatus.class;
}
}
A Timestamp Adapter
import io.littlehorse.sdk.common.adapter.LHTimestampAdapter;
import com.google.protobuf.Timestamp;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
public class ZonedDateTimeAdapter implements LHTimestampAdapter<ZonedDateTime> {
@Override
public Timestamp toTimestamp(ZonedDateTime src) {
long epochSeconds = src.toEpochSecond();
return Timestamp.newBuilder()
.setSeconds(epochSeconds)
.setNanos(src.getNano())
.build();
}
@Override
public ZonedDateTime fromTimestamp(Timestamp src) {
return ZonedDateTime.ofInstant(
java.time.Instant.ofEpochSecond(src.getSeconds(), src.getNanos()),
ZoneOffset.UTC);
}
@Override
public Class<ZonedDateTime> getTypeClass() {
return ZonedDateTime.class;
}
}
Further Resources
- Type Adapter Example — A runnable example demonstrating a UUID adapter end-to-end.
- StructDefs and Structs — Learn how structured types work in LittleHorse.
- Task Worker Development — General guide to developing task workers.
- Variables — Overview of LittleHorse variable types.