java langgraph-checkpoint

This commit is contained in:
Nuno Campos
2025-03-01 18:51:45 -08:00
parent c4275bdc32
commit 196bcfe08d
24 changed files with 2321 additions and 0 deletions
@@ -0,0 +1,60 @@
package com.langgraph.checkpoint.base;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Asynchronous interface for saving and loading checkpoints.
*/
public interface AsyncBaseCheckpointSaver {
/**
* Create a new checkpoint asynchronously.
*
* @param threadId The ID of the thread to checkpoint
* @param channelValues The values of the channels to checkpoint
* @return CompletableFuture with the ID of the new checkpoint
*/
CompletableFuture<String> checkpointAsync(String threadId, Map<String, Object> channelValues);
/**
* Get values from a checkpoint asynchronously.
*
* @param checkpointId The ID of the checkpoint to load
* @return CompletableFuture with the channel values from the checkpoint, or empty if not found
*/
CompletableFuture<Optional<Map<String, Object>>> getValuesAsync(String checkpointId);
/**
* List all checkpoints for a thread asynchronously.
*
* @param threadId The ID of the thread
* @return CompletableFuture with list of checkpoint IDs
*/
CompletableFuture<List<String>> listAsync(String threadId);
/**
* Get the latest checkpoint for a thread asynchronously.
*
* @param threadId The ID of the thread
* @return CompletableFuture with the ID of the latest checkpoint, or empty if none exists
*/
CompletableFuture<Optional<String>> latestAsync(String threadId);
/**
* Delete a checkpoint asynchronously.
*
* @param checkpointId The ID of the checkpoint to delete
* @return CompletableFuture completed when deletion is done
*/
CompletableFuture<Void> deleteAsync(String checkpointId);
/**
* Clear all checkpoints for a thread asynchronously.
*
* @param threadId The ID of the thread
* @return CompletableFuture completed when clearing is done
*/
CompletableFuture<Void> clearAsync(String threadId);
}
@@ -0,0 +1,57 @@
package com.langgraph.checkpoint.base;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Interface for saving and loading checkpoints.
*/
public interface BaseCheckpointSaver {
/**
* Create a new checkpoint.
*
* @param threadId The ID of the thread to checkpoint
* @param channelValues The values of the channels to checkpoint
* @return The ID of the new checkpoint
*/
String checkpoint(String threadId, Map<String, Object> channelValues);
/**
* Get values from a checkpoint.
*
* @param checkpointId The ID of the checkpoint to load
* @return The channel values from the checkpoint, or empty if not found
*/
Optional<Map<String, Object>> getValues(String checkpointId);
/**
* List all checkpoints for a thread.
*
* @param threadId The ID of the thread
* @return List of checkpoint IDs
*/
List<String> list(String threadId);
/**
* Get the latest checkpoint for a thread.
*
* @param threadId The ID of the thread
* @return The ID of the latest checkpoint, or empty if none exists
*/
Optional<String> latest(String threadId);
/**
* Delete a checkpoint.
*
* @param checkpointId The ID of the checkpoint to delete
*/
void delete(String checkpointId);
/**
* Clear all checkpoints for a thread.
*
* @param threadId The ID of the thread
*/
void clear(String threadId);
}
@@ -0,0 +1,81 @@
package com.langgraph.checkpoint.base;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.UUID;
/**
* Utility class for generating deterministic IDs.
*/
public final class ID {
private ID() {
// Prevent instantiation
}
/**
* Generate a deterministic UUID based on a namespace and name.
*
* @param namespace The namespace for the ID
* @param name The name within the namespace
* @return A UUID
*/
public static UUID uuid(String namespace, String name) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(namespace.getBytes(StandardCharsets.UTF_8));
md.update(name.getBytes(StandardCharsets.UTF_8));
byte[] digest = md.digest();
// Set the version (4) and variant (RFC4122) bits
digest[6] = (byte) ((digest[6] & 0x0F) | 0x40);
digest[8] = (byte) ((digest[8] & 0x3F) | 0x80);
long msb = 0;
long lsb = 0;
for (int i = 0; i < 8; i++) {
msb = (msb << 8) | (digest[i] & 0xff);
}
for (int i = 8; i < 16; i++) {
lsb = (lsb << 8) | (digest[i] & 0xff);
}
return new UUID(msb, lsb);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-1 algorithm not available", e);
}
}
/**
* Generate a checkpoint ID.
*
* @param threadId The thread ID
* @return A checkpoint ID
*/
public static String checkpointId(String threadId) {
return uuid("checkpoint", threadId + "/" + System.currentTimeMillis()).toString();
}
/**
* Generate a URL-safe base64 encoded ID.
*
* @param namespace The namespace for the ID
* @param name The name within the namespace
* @return A URL-safe base64-encoded ID
*/
public static String urlSafeId(String namespace, String name) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(namespace.getBytes(StandardCharsets.UTF_8));
md.update(name.getBytes(StandardCharsets.UTF_8));
byte[] digest = md.digest();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not available", e);
}
}
}
@@ -0,0 +1,79 @@
package com.langgraph.checkpoint.base.memory;
import com.langgraph.checkpoint.base.AsyncBaseCheckpointSaver;
import com.langgraph.checkpoint.base.BaseCheckpointSaver;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Asynchronous in-memory implementation of a checkpoint saver.
* This is a thin wrapper around the synchronous implementation that
* executes operations asynchronously.
*/
public class AsyncMemoryCheckpointSaver implements AsyncBaseCheckpointSaver {
private final BaseCheckpointSaver synchronousSaver;
/**
* Create an async memory checkpoint saver.
*/
public AsyncMemoryCheckpointSaver() {
this.synchronousSaver = new MemoryCheckpointSaver();
}
/**
* Create an async memory checkpoint saver with an existing synchronous saver.
*
* @param synchronousSaver The synchronous checkpoint saver to wrap
*/
public AsyncMemoryCheckpointSaver(BaseCheckpointSaver synchronousSaver) {
this.synchronousSaver = synchronousSaver;
}
@Override
public CompletableFuture<String> checkpointAsync(String threadId, Map<String, Object> channelValues) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.checkpoint(threadId, channelValues));
}
@Override
public CompletableFuture<Optional<Map<String, Object>>> getValuesAsync(String checkpointId) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.getValues(checkpointId));
}
@Override
public CompletableFuture<List<String>> listAsync(String threadId) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.list(threadId));
}
@Override
public CompletableFuture<Optional<String>> latestAsync(String threadId) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.latest(threadId));
}
@Override
public CompletableFuture<Void> deleteAsync(String checkpointId) {
return CompletableFuture.runAsync(() ->
synchronousSaver.delete(checkpointId));
}
@Override
public CompletableFuture<Void> clearAsync(String threadId) {
return CompletableFuture.runAsync(() ->
synchronousSaver.clear(threadId));
}
/**
* Get the underlying synchronous checkpoint saver.
*
* @return The synchronous checkpoint saver
*/
public BaseCheckpointSaver getSynchronousSaver() {
return synchronousSaver;
}
}
@@ -0,0 +1,77 @@
package com.langgraph.checkpoint.base.memory;
import com.langgraph.checkpoint.base.BaseCheckpointSaver;
import com.langgraph.checkpoint.base.ID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* In-memory implementation of a checkpoint saver.
*/
public class MemoryCheckpointSaver implements BaseCheckpointSaver {
private final Map<String, Map<String, Object>> checkpoints = new ConcurrentHashMap<>();
private final Map<String, List<String>> threadCheckpoints = new ConcurrentHashMap<>();
@Override
public String checkpoint(String threadId, Map<String, Object> channelValues) {
String checkpointId = ID.checkpointId(threadId);
// Store the checkpoint
checkpoints.put(checkpointId, new HashMap<>(channelValues));
// Add to thread's checkpoints
threadCheckpoints.computeIfAbsent(threadId, k ->
Collections.synchronizedList(new ArrayList<>())).add(checkpointId);
return checkpointId;
}
@Override
public Optional<Map<String, Object>> getValues(String checkpointId) {
Map<String, Object> values = checkpoints.get(checkpointId);
return Optional.ofNullable(values).map(HashMap::new);
}
@Override
public List<String> list(String threadId) {
List<String> result = threadCheckpoints.get(threadId);
return result != null ? new ArrayList<>(result) : Collections.emptyList();
}
@Override
public Optional<String> latest(String threadId) {
List<String> checkpoints = threadCheckpoints.get(threadId);
if (checkpoints == null || checkpoints.isEmpty()) {
return Optional.empty();
}
return Optional.of(checkpoints.get(checkpoints.size() - 1));
}
@Override
public void delete(String checkpointId) {
// Remove the checkpoint
Map<String, Object> removed = checkpoints.remove(checkpointId);
if (removed != null) {
// Find and remove from thread's checkpoints
for (List<String> checkpointsList : threadCheckpoints.values()) {
checkpointsList.remove(checkpointId);
}
}
}
@Override
public void clear(String threadId) {
List<String> checkpointIds = threadCheckpoints.remove(threadId);
if (checkpointIds != null) {
// Remove all checkpoints for this thread
for (String checkpointId : checkpointIds) {
checkpoints.remove(checkpointId);
}
}
}
}
@@ -0,0 +1,548 @@
package com.langgraph.checkpoint.serde;
import org.msgpack.core.MessageBufferPacker;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
import org.msgpack.core.MessageFormat;
import java.io.IOException;
import java.lang.reflect.*;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* MessagePack-based serializer that uses reflection to handle arbitrary Java objects.
* Supports primitive types, collections, maps, records, and custom objects.
*/
public class MsgPackSerializer implements ReflectionSerializer {
private final Map<Class<?>, TypeSerializer<?>> serializers = new ConcurrentHashMap<>();
private final Map<Class<?>, TypeDeserializer<?>> deserializers = new ConcurrentHashMap<>();
private final Map<Class<?>, RecordInfo> recordInfoCache = new ConcurrentHashMap<>();
/**
* Record component information cache to avoid repeated reflection.
*/
private static class RecordInfo {
final RecordComponent[] components;
final Constructor<?> constructor;
RecordInfo(RecordComponent[] components, Constructor<?> constructor) {
this.components = components;
this.constructor = constructor;
}
}
/**
* Register built-in serializers for common types.
*/
public MsgPackSerializer() {
registerBuiltinTypes();
}
/**
* Register built-in serializers for common types.
*/
private void registerBuiltinTypes() {
// UUID serializer
registerSerializer(UUID.class, (uuid) -> uuid.toString());
registerDeserializer(UUID.class, (str) -> UUID.fromString((String) str));
// Date serializer
registerSerializer(java.util.Date.class, (date) -> date.getTime());
registerDeserializer(java.util.Date.class, (millis) -> new Date((Long) millis));
// Java 8 Date/Time API
registerSerializer(Instant.class, (instant) -> instant.toString());
registerDeserializer(Instant.class, (str) -> Instant.parse((String) str));
registerSerializer(LocalDate.class, (date) -> date.toString());
registerDeserializer(LocalDate.class, (str) -> LocalDate.parse((String) str));
registerSerializer(LocalTime.class, (time) -> time.toString());
registerDeserializer(LocalTime.class, (str) -> LocalTime.parse((String) str));
registerSerializer(LocalDateTime.class, (dateTime) -> dateTime.toString());
registerDeserializer(LocalDateTime.class, (str) -> LocalDateTime.parse((String) str));
// Add more built-in serializers as needed
}
@Override
public <T> void registerSerializer(Class<T> type, TypeSerializer<T> serializer) {
serializers.put(type, serializer);
}
@Override
public <T> void registerDeserializer(Class<T> type, TypeDeserializer<T> deserializer) {
deserializers.put(type, deserializer);
}
@Override
public byte[] serialize(Object obj) {
try {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
serializeObject(obj, packer);
return packer.toByteArray();
} catch (IOException e) {
throw new SerializationException("Failed to serialize object", e);
}
}
@Override
public Object deserialize(byte[] data) {
try {
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data);
return deserializeObject(unpacker);
} catch (IOException e) {
throw new SerializationException("Failed to deserialize object", e);
}
}
/**
* Serialize an object to the MessagePack packer.
*
* @param obj Object to serialize
* @param packer MessagePack packer
* @throws IOException If packing fails
*/
@SuppressWarnings("unchecked")
private void serializeObject(Object obj, MessageBufferPacker packer) throws IOException {
if (obj == null) {
packer.packNil();
return;
}
Class<?> type = obj.getClass();
// Check for registered serializer
if (serializers.containsKey(type)) {
TypeSerializer<Object> serializer = (TypeSerializer<Object>) serializers.get(type);
Object serialized = serializer.toSerializable(obj);
// Pack as a special type
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(type.getName());
packer.packString("value");
serializeObject(serialized, packer);
return;
}
// Handle primitive types and common objects directly
if (obj instanceof String) {
packer.packString((String) obj);
} else if (obj instanceof Integer) {
packer.packInt((Integer) obj);
} else if (obj instanceof Long) {
packer.packLong((Long) obj);
} else if (obj instanceof Double) {
packer.packDouble((Double) obj);
} else if (obj instanceof Float) {
packer.packFloat((Float) obj);
} else if (obj instanceof Boolean) {
packer.packBoolean((Boolean) obj);
} else if (obj instanceof byte[]) {
packer.packBinaryHeader(((byte[]) obj).length);
packer.writePayload((byte[]) obj);
} else if (obj instanceof List) {
List<?> list = (List<?>) obj;
packer.packArrayHeader(list.size());
for (Object item : list) {
serializeObject(item, packer);
}
} else if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
packer.packMapHeader(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
serializeObject(entry.getKey(), packer);
serializeObject(entry.getValue(), packer);
}
} else if (obj instanceof Enum<?>) {
// Handle enums by name
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(type.getName());
packer.packString("value");
packer.packString(((Enum<?>) obj).name());
} else if (type.isRecord()) {
// Handle Record types
serializeRecord(obj, packer);
} else {
// Handle custom objects with reflection
serializeCustomObject(obj, packer);
}
}
/**
* Serialize a Record object.
*
* @param record The record to serialize
* @param packer The MessagePack packer
* @throws IOException If packing fails
*/
private void serializeRecord(Object record, MessageBufferPacker packer) throws IOException {
Class<?> recordClass = record.getClass();
// Pack as a special type with fields
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(recordClass.getName());
packer.packString("fields");
RecordComponent[] components = recordClass.getRecordComponents();
packer.packMapHeader(components.length);
for (RecordComponent component : components) {
packer.packString(component.getName());
try {
Method accessor = component.getAccessor();
Object value = accessor.invoke(record);
serializeObject(value, packer);
} catch (ReflectiveOperationException e) {
throw new SerializationException("Failed to access record component: " + component.getName(), e);
}
}
}
/**
* Serialize a custom object using reflection.
*
* @param obj The object to serialize
* @param packer The MessagePack packer
* @throws IOException If packing fails
*/
private void serializeCustomObject(Object obj, MessageBufferPacker packer) throws IOException {
Class<?> objClass = obj.getClass();
// Pack as a special type with fields
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(objClass.getName());
packer.packString("fields");
// Get all fields including inherited ones
List<Field> fields = getAllFields(objClass);
// Filter out transient fields
List<Field> serializableFields = fields.stream()
.filter(field -> !Modifier.isTransient(field.getModifiers()) &&
!Modifier.isStatic(field.getModifiers()))
.toList();
packer.packMapHeader(serializableFields.size());
for (Field field : serializableFields) {
packer.packString(field.getName());
try {
field.setAccessible(true);
Object value = field.get(obj);
serializeObject(value, packer);
} catch (IllegalAccessException e) {
throw new SerializationException("Failed to access field: " + field.getName(), e);
}
}
}
/**
* Get all fields for a class including inherited fields.
*
* @param clazz The class to get fields for
* @return List of all fields
*/
private List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
currentClass = currentClass.getSuperclass();
}
return fields;
}
/**
* Deserialize an object from the MessagePack unpacker.
*
* @param unpacker MessagePack unpacker
* @return Deserialized object
* @throws IOException If unpacking fails
*/
@SuppressWarnings("unchecked")
private Object deserializeObject(MessageUnpacker unpacker) throws IOException {
if (!unpacker.hasNext()) {
throw new SerializationException("Unexpected end of data");
}
if (unpacker.tryUnpackNil()) {
return null;
}
MessageFormat format = unpacker.getNextFormat();
if (format == MessageFormat.STR8 ||
format == MessageFormat.STR16 ||
format == MessageFormat.STR32 ||
format == MessageFormat.FIXSTR) {
return unpacker.unpackString();
} else if (format == MessageFormat.INT8 ||
format == MessageFormat.INT16 ||
format == MessageFormat.INT32 ||
format == MessageFormat.INT64 ||
format == MessageFormat.UINT8 ||
format == MessageFormat.UINT16 ||
format == MessageFormat.UINT32 ||
format == MessageFormat.UINT64 ||
format == MessageFormat.POSFIXINT ||
format == MessageFormat.NEGFIXINT) {
if (format == MessageFormat.INT64 || format == MessageFormat.UINT64) {
return unpacker.unpackLong();
} else {
try {
return unpacker.unpackInt();
} catch (Exception e) {
// Fallback to long if int unpacking fails
return unpacker.unpackLong();
}
}
} else if (format == MessageFormat.FLOAT32 ||
format == MessageFormat.FLOAT64) {
return unpacker.unpackDouble();
} else if (format == MessageFormat.BOOLEAN) {
return unpacker.unpackBoolean();
} else if (format == MessageFormat.BIN8 ||
format == MessageFormat.BIN16 ||
format == MessageFormat.BIN32) {
int binaryLength = unpacker.unpackBinaryHeader();
byte[] binary = new byte[binaryLength];
unpacker.readPayload(binary);
return binary;
} else if (format == MessageFormat.ARRAY16 ||
format == MessageFormat.ARRAY32 ||
format == MessageFormat.FIXARRAY) {
int arraySize = unpacker.unpackArrayHeader();
List<Object> list = new ArrayList<>(arraySize);
for (int i = 0; i < arraySize; i++) {
list.add(deserializeObject(unpacker));
}
return list;
} else if (format == MessageFormat.MAP16 ||
format == MessageFormat.MAP32 ||
format == MessageFormat.FIXMAP) {
int mapSize = unpacker.unpackMapHeader();
// Handle empty map
if (mapSize == 0) {
return new HashMap<>();
}
// Check for special type marker
Object firstKey = deserializeObject(unpacker);
if (mapSize == 2 && firstKey instanceof String && "__type__".equals(firstKey)) {
String typeName = (String) deserializeObject(unpacker);
// Get the second key
Object secondKey = deserializeObject(unpacker);
if (secondKey instanceof String) {
String secondKeyStr = (String) secondKey;
try {
Class<?> type = Class.forName(typeName);
// Check for registered deserializer
if ("value".equals(secondKeyStr) && deserializers.containsKey(type)) {
Object serialized = deserializeObject(unpacker);
TypeDeserializer<Object> deserializer =
(TypeDeserializer<Object>) deserializers.get(type);
return deserializer.fromSerialized(serialized);
}
// Handle enums
if ("value".equals(secondKeyStr) && type.isEnum()) {
String enumValue = (String) deserializeObject(unpacker);
return Enum.valueOf((Class<Enum>) type, enumValue);
}
// Handle records
if ("fields".equals(secondKeyStr) && type.isRecord()) {
return deserializeRecord(type, unpacker);
}
// Handle custom objects
if ("fields".equals(secondKeyStr)) {
return deserializeCustomObject(type, unpacker);
}
} catch (ClassNotFoundException e) {
// If class not found, fall back to regular map deserialization
} catch (ReflectiveOperationException e) {
throw new SerializationException("Failed to deserialize object of type " + typeName, e);
}
// If special type handling failed, read the value to keep unpacker consistent
Object secondValue = deserializeObject(unpacker);
// Create a fallback map with the special type info
Map<Object, Object> fallbackMap = new HashMap<>();
fallbackMap.put(firstKey, typeName);
fallbackMap.put(secondKey, secondValue);
return fallbackMap;
}
// If the second key wasn't a string as expected, we need to handle it as a regular map
Object firstValue = deserializeObject(unpacker);
// Create a map with the first key-value pair
Map<Object, Object> map = new HashMap<>(mapSize);
map.put(firstKey, firstValue);
// Read the remaining entries
for (int i = 1; i < mapSize; i++) {
Object key = deserializeObject(unpacker);
Object value = deserializeObject(unpacker);
map.put(key, value);
}
return map;
} else {
// Regular map - we already read the first key
Map<Object, Object> map = new HashMap<>(mapSize);
// Read the first value
Object firstValue = deserializeObject(unpacker);
map.put(firstKey, firstValue);
// Read the remaining entries
for (int i = 1; i < mapSize; i++) {
Object key = deserializeObject(unpacker);
Object value = deserializeObject(unpacker);
map.put(key, value);
}
return map;
}
}
// Default case
throw new SerializationException("Unsupported MessagePack format: " + format);
}
/**
* Deserialize a record.
*
* @param recordClass The record class
* @param unpacker The unpacker containing the fields map
* @return The deserialized record
* @throws IOException If unpacking fails
* @throws ReflectiveOperationException If reflection operations fail
*/
private Object deserializeRecord(Class<?> recordClass, MessageUnpacker unpacker)
throws IOException, ReflectiveOperationException {
// Get record info from cache or create it
RecordInfo recordInfo = recordInfoCache.computeIfAbsent(recordClass, cls -> {
try {
RecordComponent[] components = cls.getRecordComponents();
Class<?>[] paramTypes = Arrays.stream(components)
.map(RecordComponent::getType)
.toArray(Class<?>[]::new);
Constructor<?> constructor = cls.getDeclaredConstructor(paramTypes);
constructor.setAccessible(true);
return new RecordInfo(components, constructor);
} catch (NoSuchMethodException e) {
throw new SerializationException("Failed to get constructor for record: " + cls.getName(), e);
}
});
// Read the fields map
int fieldCount = unpacker.unpackMapHeader();
Map<String, Object> fieldValues = new HashMap<>(fieldCount);
for (int i = 0; i < fieldCount; i++) {
String fieldName = (String) deserializeObject(unpacker);
Object fieldValue = deserializeObject(unpacker);
fieldValues.put(fieldName, fieldValue);
}
// Prepare constructor arguments in the correct order
Object[] constructorArgs = new Object[recordInfo.components.length];
for (int i = 0; i < recordInfo.components.length; i++) {
RecordComponent component = recordInfo.components[i];
Object value = fieldValues.get(component.getName());
constructorArgs[i] = value;
}
// Create the record instance
return recordInfo.constructor.newInstance(constructorArgs);
}
/**
* Deserialize a custom object.
*
* @param objectClass The object class
* @param unpacker The unpacker containing the fields map
* @return The deserialized object
* @throws IOException If unpacking fails
* @throws ReflectiveOperationException If reflection operations fail
*/
private Object deserializeCustomObject(Class<?> objectClass, MessageUnpacker unpacker)
throws IOException, ReflectiveOperationException {
// Create instance using default constructor
Constructor<?> constructor;
try {
constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new SerializationException(
"Class " + objectClass.getName() + " must have a no-arg constructor for deserialization", e);
}
Object instance = constructor.newInstance();
// Read the fields map
int fieldCount = unpacker.unpackMapHeader();
for (int i = 0; i < fieldCount; i++) {
String fieldName = (String) deserializeObject(unpacker);
Object fieldValue = deserializeObject(unpacker);
try {
// Find the field (including in superclasses)
Field field = findField(objectClass, fieldName);
if (field != null) {
field.setAccessible(true);
field.set(instance, fieldValue);
}
} catch (NoSuchFieldException e) {
// Skip fields that don't exist in the current class version
}
}
return instance;
}
/**
* Find a field in a class or its superclasses.
*
* @param clazz The class to search
* @param fieldName The field name to find
* @return The found field
* @throws NoSuchFieldException If the field is not found
*/
private Field findField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
Class<?> currentClass = clazz;
while (currentClass != null) {
try {
return currentClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
currentClass = currentClass.getSuperclass();
}
}
throw new NoSuchFieldException("Field not found: " + fieldName);
}
}
@@ -0,0 +1,24 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for a serializer that uses reflection to handle arbitrary Java objects.
*/
public interface ReflectionSerializer extends Serializer<Object> {
/**
* Register a custom serializer for a specific type.
*
* @param type Type to register
* @param serializer Custom serializer for the type
* @param <T> Type to register
*/
<T> void registerSerializer(Class<T> type, TypeSerializer<T> serializer);
/**
* Register a custom deserializer for a specific type.
*
* @param type Type to register
* @param deserializer Custom deserializer for the type
* @param <T> Type to register
*/
<T> void registerDeserializer(Class<T> type, TypeDeserializer<T> deserializer);
}
@@ -0,0 +1,25 @@
package com.langgraph.checkpoint.serde;
/**
* Exception thrown during serialization/deserialization.
*/
public class SerializationException extends RuntimeException {
/**
* Create a new serialization exception with a message.
*
* @param message Error message
*/
public SerializationException(String message) {
super(message);
}
/**
* Create a new serialization exception with a message and cause.
*
* @param message Error message
* @param cause Underlying cause
*/
public SerializationException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,24 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for serializing and deserializing objects.
*
* @param <T> Type of object to serialize/deserialize
*/
public interface Serializer<T> {
/**
* Serialize an object to bytes.
*
* @param obj The object to serialize
* @return Serialized bytes
*/
byte[] serialize(T obj);
/**
* Deserialize bytes to an object.
*
* @param data The bytes to deserialize
* @return Deserialized object
*/
T deserialize(byte[] data);
}
@@ -0,0 +1,17 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for deserializing a specific type from MessagePack.
*
* @param <T> Type to deserialize
*/
@FunctionalInterface
public interface TypeDeserializer<T> {
/**
* Convert from serialized representation to object.
*
* @param serialized Serialized representation
* @return Deserialized object
*/
T fromSerialized(Object serialized);
}
@@ -0,0 +1,17 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for serializing a specific type to a format that can be included in MessagePack.
*
* @param <T> Type to serialize
*/
@FunctionalInterface
public interface TypeSerializer<T> {
/**
* Convert object to a serializable representation.
*
* @param obj Object to convert
* @return Serializable representation (must be compatible with MessagePack)
*/
Object toSerializable(T obj);
}
@@ -0,0 +1,59 @@
package com.langgraph.checkpoint.base;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
public class IDTest {
@Test
public void testUuidDeterministic() {
// Same inputs should produce same UUIDs
UUID uuid1 = ID.uuid("test", "value");
UUID uuid2 = ID.uuid("test", "value");
assertThat(uuid1).isEqualTo(uuid2);
}
@Test
public void testUuidDifferentNamespace() {
// Different namespaces should produce different UUIDs
UUID uuid1 = ID.uuid("namespace1", "value");
UUID uuid2 = ID.uuid("namespace2", "value");
assertThat(uuid1).isNotEqualTo(uuid2);
}
@Test
public void testUuidDifferentName() {
// Different names should produce different UUIDs
UUID uuid1 = ID.uuid("test", "value1");
UUID uuid2 = ID.uuid("test", "value2");
assertThat(uuid1).isNotEqualTo(uuid2);
}
@Test
public void testCheckpointId() {
// Checkpoint IDs should be valid UUIDs
String id = ID.checkpointId("thread-123");
// Should be a valid UUID string
UUID uuid = UUID.fromString(id);
assertThat(uuid).isNotNull();
}
@Test
public void testUrlSafeId() {
// URL-safe IDs should be deterministic
String id1 = ID.urlSafeId("test", "value");
String id2 = ID.urlSafeId("test", "value");
assertThat(id1).isEqualTo(id2);
// Should not contain padding characters or unsafe URL characters
assertThat(id1).doesNotContain("=", "+", "/");
}
}
@@ -0,0 +1,155 @@
package com.langgraph.checkpoint.base.memory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
public class AsyncMemoryCheckpointSaverTest {
private AsyncMemoryCheckpointSaver saver;
@BeforeEach
public void setUp() {
saver = new AsyncMemoryCheckpointSaver();
}
@Test
public void testCheckpointAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint asynchronously
CompletableFuture<String> future = saver.checkpointAsync(threadId, values);
// Wait for completion
String checkpointId = future.get();
// Verify checkpoint ID format (should be a UUID)
assertThat(checkpointId).matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
// Verify thread has a checkpoint
CompletableFuture<List<String>> listFuture = saver.listAsync(threadId);
List<String> checkpoints = listFuture.get();
assertThat(checkpoints).hasSize(1);
assertThat(checkpoints).contains(checkpointId);
}
@Test
public void testGetValuesAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint
String checkpointId = saver.checkpointAsync(threadId, values).get();
// Get values asynchronously
CompletableFuture<Optional<Map<String, Object>>> future = saver.getValuesAsync(checkpointId);
Optional<Map<String, Object>> retrievedValues = future.get();
// Verify values
assertThat(retrievedValues).isPresent();
assertThat(retrievedValues.get()).containsEntry("key1", "value1");
assertThat(retrievedValues.get()).containsEntry("key2", 42);
}
@Test
public void testListAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Initially empty
CompletableFuture<List<String>> initialFuture = saver.listAsync(threadId);
List<String> initial = initialFuture.get();
assertThat(initial).isEmpty();
// Create multiple checkpoints
String id1 = saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
String id2 = saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
String id3 = saver.checkpointAsync(threadId, Map.of("key", "value3")).get();
// List checkpoints asynchronously
CompletableFuture<List<String>> future = saver.listAsync(threadId);
List<String> checkpoints = future.get();
// Verify order and content
assertThat(checkpoints).hasSize(3);
assertThat(checkpoints).containsExactly(id1, id2, id3);
}
@Test
public void testLatestAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Initially empty
CompletableFuture<Optional<String>> initialFuture = saver.latestAsync(threadId);
Optional<String> initial = initialFuture.get();
assertThat(initial).isEmpty();
// Create multiple checkpoints
saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
String id3 = saver.checkpointAsync(threadId, Map.of("key", "value3")).get();
// Get latest asynchronously
CompletableFuture<Optional<String>> future = saver.latestAsync(threadId);
Optional<String> latest = future.get();
// Verify latest
assertThat(latest).isPresent();
assertThat(latest.get()).isEqualTo(id3);
}
@Test
public void testDeleteAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Create checkpoint
String checkpointId = saver.checkpointAsync(threadId, Map.of("key", "value")).get();
// Verify checkpoint exists
assertThat(saver.getValuesAsync(checkpointId).get()).isPresent();
// Delete checkpoint asynchronously
CompletableFuture<Void> future = saver.deleteAsync(checkpointId);
future.get(); // Wait for completion
// Verify checkpoint is deleted
assertThat(saver.getValuesAsync(checkpointId).get()).isEmpty();
}
@Test
public void testClearAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Create multiple checkpoints
String id1 = saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
String id2 = saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
// Verify checkpoints exist
assertThat(saver.listAsync(threadId).get()).hasSize(2);
// Clear thread asynchronously
CompletableFuture<Void> future = saver.clearAsync(threadId);
future.get(); // Wait for completion
// Verify checkpoints are deleted
assertThat(saver.listAsync(threadId).get()).isEmpty();
}
}
@@ -0,0 +1,161 @@
package com.langgraph.checkpoint.base.memory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
public class MemoryCheckpointSaverTest {
private MemoryCheckpointSaver saver;
@BeforeEach
public void setUp() {
saver = new MemoryCheckpointSaver();
}
@Test
public void testCheckpoint() {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint
String checkpointId = saver.checkpoint(threadId, values);
// Verify checkpoint ID format (should be a UUID)
assertThat(checkpointId).matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
// Verify thread has a checkpoint
List<String> checkpoints = saver.list(threadId);
assertThat(checkpoints).hasSize(1);
assertThat(checkpoints).contains(checkpointId);
// Verify latest checkpoint
Optional<String> latest = saver.latest(threadId);
assertThat(latest).isPresent();
assertThat(latest.get()).isEqualTo(checkpointId);
}
@Test
public void testGetValues() {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint
String checkpointId = saver.checkpoint(threadId, values);
// Get values
Optional<Map<String, Object>> retrievedValues = saver.getValues(checkpointId);
// Verify values
assertThat(retrievedValues).isPresent();
assertThat(retrievedValues.get()).containsEntry("key1", "value1");
assertThat(retrievedValues.get()).containsEntry("key2", 42);
// Verify non-existent checkpoint
Optional<Map<String, Object>> nonExistent = saver.getValues("non-existent");
assertThat(nonExistent).isEmpty();
}
@Test
public void testList() {
// Create test data
String threadId = "test-thread";
// Initially empty
List<String> initial = saver.list(threadId);
assertThat(initial).isEmpty();
// Create multiple checkpoints
String id1 = saver.checkpoint(threadId, Map.of("key", "value1"));
String id2 = saver.checkpoint(threadId, Map.of("key", "value2"));
String id3 = saver.checkpoint(threadId, Map.of("key", "value3"));
// List checkpoints
List<String> checkpoints = saver.list(threadId);
// Verify order and content
assertThat(checkpoints).hasSize(3);
assertThat(checkpoints).containsExactly(id1, id2, id3);
// Different thread should have no checkpoints
List<String> otherThread = saver.list("other-thread");
assertThat(otherThread).isEmpty();
}
@Test
public void testLatest() {
// Create test data
String threadId = "test-thread";
// Initially empty
Optional<String> initial = saver.latest(threadId);
assertThat(initial).isEmpty();
// Create multiple checkpoints
saver.checkpoint(threadId, Map.of("key", "value1"));
saver.checkpoint(threadId, Map.of("key", "value2"));
String id3 = saver.checkpoint(threadId, Map.of("key", "value3"));
// Get latest
Optional<String> latest = saver.latest(threadId);
// Verify latest
assertThat(latest).isPresent();
assertThat(latest.get()).isEqualTo(id3);
}
@Test
public void testDelete() {
// Create test data
String threadId = "test-thread";
// Create checkpoint
String checkpointId = saver.checkpoint(threadId, Map.of("key", "value"));
// Verify checkpoint exists
assertThat(saver.getValues(checkpointId)).isPresent();
assertThat(saver.list(threadId)).contains(checkpointId);
// Delete checkpoint
saver.delete(checkpointId);
// Verify checkpoint is deleted
assertThat(saver.getValues(checkpointId)).isEmpty();
assertThat(saver.list(threadId)).doesNotContain(checkpointId);
}
@Test
public void testClear() {
// Create test data
String threadId = "test-thread";
// Create multiple checkpoints
String id1 = saver.checkpoint(threadId, Map.of("key", "value1"));
String id2 = saver.checkpoint(threadId, Map.of("key", "value2"));
// Verify checkpoints exist
assertThat(saver.list(threadId)).hasSize(2);
assertThat(saver.getValues(id1)).isPresent();
assertThat(saver.getValues(id2)).isPresent();
// Clear thread
saver.clear(threadId);
// Verify checkpoints are deleted
assertThat(saver.list(threadId)).isEmpty();
assertThat(saver.getValues(id1)).isEmpty();
assertThat(saver.getValues(id2)).isEmpty();
}
}
@@ -0,0 +1,383 @@
package com.langgraph.checkpoint.serde;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
public class MsgPackSerializerTest {
private MsgPackSerializer serializer;
@BeforeEach
public void setUp() {
serializer = new MsgPackSerializer();
}
@Test
public void testSerializeDeserializePrimitives() {
// Test with various primitive types
assertRoundTrip("Test string");
assertRoundTrip(123);
assertRoundTrip(123456789L);
assertRoundTrip(123.45);
assertRoundTrip(123.45f);
assertRoundTrip(true);
assertRoundTrip(false);
assertRoundTrip(null);
}
@Test
public void testSerializeDeserializeArrays() {
// Test with arrays and collections
assertRoundTrip(new byte[] {1, 2, 3, 4, 5});
assertRoundTrip(Arrays.asList("one", "two", "three"));
assertRoundTrip(Arrays.asList(1, 2, 3, 4, 5));
}
@Test
public void testSerializeDeserializeMaps() {
// Test with maps
Map<String, Object> map = new HashMap<>();
map.put("string", "value");
map.put("int", 123);
map.put("boolean", true);
assertRoundTrip(map);
}
@Test
public void testSerializeDeserializeNestedStructures() {
// Test with nested structures
Map<String, Object> nested = new HashMap<>();
nested.put("list", Arrays.asList(1, 2, 3));
nested.put("map", Map.of("key", "value"));
assertRoundTrip(nested);
}
@Test
public void testSerializeDeserializeEnums() {
// Test with enums
assertRoundTrip(TestEnum.VALUE1);
assertRoundTrip(TestEnum.VALUE2);
assertRoundTrip(TestEnum.VALUE3);
}
@Test
public void testSerializeDeserializeRecord() {
// Test with a record
TestRecord record = new TestRecord("test", 123, Arrays.asList("a", "b", "c"));
// Serialize and deserialize
byte[] serialized = serializer.serialize(record);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(TestRecord.class);
TestRecord deserializedRecord = (TestRecord) deserialized;
assertThat(deserializedRecord.name()).isEqualTo("test");
assertThat(deserializedRecord.value()).isEqualTo(123);
assertThat(deserializedRecord.tags()).containsExactly("a", "b", "c");
}
@Test
public void testSerializeDeserializeNestedRecord() {
// Test with a nested record
NestedTestRecord record = new NestedTestRecord(
"parent",
new TestRecord("child", 456, Arrays.asList("x", "y", "z"))
);
// Serialize and deserialize
byte[] serialized = serializer.serialize(record);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(NestedTestRecord.class);
NestedTestRecord deserializedRecord = (NestedTestRecord) deserialized;
assertThat(deserializedRecord.name()).isEqualTo("parent");
assertThat(deserializedRecord.child()).isInstanceOf(TestRecord.class);
assertThat(deserializedRecord.child().name()).isEqualTo("child");
assertThat(deserializedRecord.child().value()).isEqualTo(456);
assertThat(deserializedRecord.child().tags()).containsExactly("x", "y", "z");
}
@Test
public void testSerializeDeserializeCustomObject() {
// Test with a custom object
TestObject obj = new TestObject();
obj.setName("test");
obj.setValue(123);
obj.setActive(true);
// Serialize and deserialize
byte[] serialized = serializer.serialize(obj);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(TestObject.class);
TestObject deserializedObj = (TestObject) deserialized;
assertThat(deserializedObj.getName()).isEqualTo("test");
assertThat(deserializedObj.getValue()).isEqualTo(123);
assertThat(deserializedObj.isActive()).isTrue();
}
@Test
public void testSerializeDeserializeInheritance() {
// Test with inheritance
ChildTestObject obj = new ChildTestObject();
obj.setName("parent");
obj.setValue(123);
obj.setActive(true);
obj.setChildProperty("child");
obj.setChildValue(456);
// Serialize and deserialize
byte[] serialized = serializer.serialize(obj);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(ChildTestObject.class);
ChildTestObject deserializedObj = (ChildTestObject) deserialized;
assertThat(deserializedObj.getName()).isEqualTo("parent");
assertThat(deserializedObj.getValue()).isEqualTo(123);
assertThat(deserializedObj.isActive()).isTrue();
assertThat(deserializedObj.getChildProperty()).isEqualTo("child");
assertThat(deserializedObj.getChildValue()).isEqualTo(456);
}
@Test
public void testSerializeDeserializeWithCustomSerializer() {
// Register custom UUID serializer (although built-in one exists)
serializer.registerSerializer(UUID.class, (uuid) -> uuid.toString().replace("-", ""));
serializer.registerDeserializer(UUID.class, (str) -> {
String uuidStr = (String) str;
// Insert hyphens for standard UUID format
uuidStr = uuidStr.replaceFirst(
"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)",
"$1-$2-$3-$4-$5");
return UUID.fromString(uuidStr);
});
// Test with UUID
UUID uuid = UUID.randomUUID();
// Serialize and deserialize
byte[] serialized = serializer.serialize(uuid);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(UUID.class);
assertThat(deserialized).isEqualTo(uuid);
}
@Test
public void testSerializeDeserializeDateTypes() {
// Test with Date
Date date = new Date();
// Serialize and deserialize
byte[] serialized = serializer.serialize(date);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(Date.class);
assertThat(deserialized).isEqualTo(date);
// Test with Java 8 Date/Time types
Instant instant = Instant.now();
LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();
assertRoundTrip(instant);
assertRoundTrip(localDate);
assertRoundTrip(localTime);
assertRoundTrip(localDateTime);
}
@Test
public void testTransientFields() {
// Test with transient fields
ObjectWithTransient obj = new ObjectWithTransient();
obj.setPersistent("saved");
obj.setTransientField("not-saved");
// Serialize and deserialize
byte[] serialized = serializer.serialize(obj);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(ObjectWithTransient.class);
ObjectWithTransient deserializedObj = (ObjectWithTransient) deserialized;
assertThat(deserializedObj.getPersistent()).isEqualTo("saved");
assertThat(deserializedObj.getTransientField()).isNull(); // Should be null after deserialization
}
/**
* Helper method to assert that an object survives a round trip through serialization.
*
* @param obj Object to test
*/
private void assertRoundTrip(Object obj) {
try {
// Serialize
byte[] serialized = serializer.serialize(obj);
// Deserialize
Object deserialized = serializer.deserialize(serialized);
// Verify
if (obj instanceof byte[]) {
// Arrays need special comparison
assertThat(deserialized).isInstanceOf(byte[].class);
assertThat((byte[]) deserialized).isEqualTo((byte[]) obj);
} else if (obj instanceof Number) {
// For any number type, compare by value instead of exact type
if (deserialized instanceof Number) {
double expected = ((Number) obj).doubleValue();
double actual = ((Number) deserialized).doubleValue();
assertThat(actual).isCloseTo(expected, within(0.0001));
} else {
throw new AssertionError("Expected Number, got " +
(deserialized != null ? deserialized.getClass().getName() : "null"));
}
} else {
// Special handling for lists
if (obj instanceof List && deserialized instanceof List) {
List<?> originalList = (List<?>) obj;
List<?> deserializedList = (List<?>) deserialized;
assertThat(deserializedList).hasSameSizeAs(originalList);
// Check each element
for (int i = 0; i < originalList.size(); i++) {
Object originalItem = originalList.get(i);
Object deserializedItem = deserializedList.get(i);
if (originalItem instanceof Number && deserializedItem instanceof Number) {
// Compare numbers by value instead of exact type
assertThat(((Number) deserializedItem).doubleValue())
.isCloseTo(((Number) originalItem).doubleValue(), within(0.0001));
} else {
assertThat(deserializedItem).isEqualTo(originalItem);
}
}
} else {
// Regular equality for other types
assertThat(deserialized).isEqualTo(obj);
}
}
} catch (Exception e) {
throw new AssertionError("Error in roundtrip for " + obj + ": " + e.getMessage(), e);
}
}
/**
* Test enum.
*/
public enum TestEnum {
VALUE1, VALUE2, VALUE3
}
/**
* Test record class.
*/
public record TestRecord(String name, int value, List<String> tags) {
}
/**
* Nested test record class.
*/
public record NestedTestRecord(String name, TestRecord child) {
}
/**
* Test class for custom object serialization.
*/
public static class TestObject {
private String name;
private int value;
private boolean active;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
/**
* Child test class for inheritance testing.
*/
public static class ChildTestObject extends TestObject {
private String childProperty;
private int childValue;
public String getChildProperty() {
return childProperty;
}
public void setChildProperty(String childProperty) {
this.childProperty = childProperty;
}
public int getChildValue() {
return childValue;
}
public void setChildValue(int childValue) {
this.childValue = childValue;
}
}
/**
* Test class with transient fields.
*/
public static class ObjectWithTransient {
private String persistent;
private transient String transientField;
public String getPersistent() {
return persistent;
}
public void setPersistent(String persistent) {
this.persistent = persistent;
}
public String getTransientField() {
return transientField;
}
public void setTransientField(String transientField) {
this.transientField = transientField;
}
}
}