Protobuf serialization for pre-existing objects with runtime schema generation and polymorphic type handling
—
System for implementing custom serialization logic for specific types, providing higher priority than default serializers. The delegate system allows you to override the default serialization behavior for any type, enabling special handling for complex objects, third-party types, or optimization scenarios.
Core interface for implementing custom type serialization with full control over the serialization process.
/**
* Interface for custom type serialization delegates
* Delegates have higher priority than default serializers
*/
public interface Delegate<V> {
/**
* Get the protobuf field type for this delegate
* @return FieldType for wire format
*/
FieldType getFieldType();
/**
* Read/deserialize value from input stream
* @param input Input stream containing serialized data
* @return Deserialized value instance
* @throws IOException if reading fails
*/
V readFrom(Input input) throws IOException;
/**
* Write/serialize value to output stream
* @param output Output stream to write to
* @param number Field number for this value
* @param value Value to serialize
* @param repeated Whether this is a repeated field
* @throws IOException if writing fails
*/
void writeTo(Output output, int number, V value, boolean repeated) throws IOException;
/**
* Transfer value through a pipe (streaming serialization)
* @param pipe Pipe for streaming operations
* @param input Input stream
* @param output Output stream
* @param number Field number
* @param repeated Whether this is a repeated field
* @throws IOException if transfer fails
*/
void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated) throws IOException;
/**
* Get the Java class this delegate handles
* @return Class type handled by this delegate
*/
Class<?> typeClass();
}Wrapper class for delegates in polymorphic contexts, providing access to the underlying delegate.
/**
* Wrapper for delegates in polymorphic serialization contexts
*/
public class HasDelegate<T> {
/**
* Get the wrapped delegate instance
* @return Delegate instance
*/
public Delegate<T> getDelegate();
}import io.protostuff.runtime.Delegate;
import io.protostuff.WireFormat.FieldType;
import io.protostuff.Input;
import io.protostuff.Output;
import io.protostuff.Pipe;
import java.io.IOException;
/**
* Custom delegate for String serialization with compression
*/
public class CompressedStringDelegate implements Delegate<String> {
@Override
public FieldType getFieldType() {
return FieldType.BYTES; // Serialize as bytes for compression
}
@Override
public String readFrom(Input input) throws IOException {
byte[] compressed = input.readByteArray();
return decompress(compressed);
}
@Override
public void writeTo(Output output, int number, String value, boolean repeated) throws IOException {
if (value != null) {
byte[] compressed = compress(value);
output.writeByteArray(number, compressed, repeated);
}
}
@Override
public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated)
throws IOException {
output.writeByteArray(number, input.readByteArray(), repeated);
}
@Override
public Class<?> typeClass() {
return String.class;
}
private byte[] compress(String value) {
// Implement compression logic
return value.getBytes(); // Simplified
}
private String decompress(byte[] compressed) {
// Implement decompression logic
return new String(compressed); // Simplified
}
}import java.util.Date;
/**
* Custom delegate for Date serialization as long timestamp
*/
public class DateDelegate implements Delegate<Date> {
@Override
public FieldType getFieldType() {
return FieldType.INT64;
}
@Override
public Date readFrom(Input input) throws IOException {
long timestamp = input.readInt64();
return new Date(timestamp);
}
@Override
public void writeTo(Output output, int number, Date value, boolean repeated) throws IOException {
if (value != null) {
output.writeInt64(number, value.getTime(), repeated);
}
}
@Override
public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated)
throws IOException {
output.writeInt64(number, input.readInt64(), repeated);
}
@Override
public Class<?> typeClass() {
return Date.class;
}
}/**
* Custom delegate for enum serialization by name with fallback
*/
public class SafeEnumDelegate<E extends Enum<E>> implements Delegate<E> {
private final Class<E> enumClass;
private final E defaultValue;
public SafeEnumDelegate(Class<E> enumClass, E defaultValue) {
this.enumClass = enumClass;
this.defaultValue = defaultValue;
}
@Override
public FieldType getFieldType() {
return FieldType.STRING;
}
@Override
public E readFrom(Input input) throws IOException {
String name = input.readString();
try {
return Enum.valueOf(enumClass, name);
} catch (IllegalArgumentException e) {
return defaultValue; // Fallback for unknown values
}
}
@Override
public void writeTo(Output output, int number, E value, boolean repeated) throws IOException {
if (value != null) {
output.writeString(number, value.name(), repeated);
}
}
@Override
public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated)
throws IOException {
output.writeString(number, input.readString(), repeated);
}
@Override
public Class<?> typeClass() {
return enumClass;
}
}import java.util.*;
/**
* Custom delegate for Set serialization with duplicate detection
*/
public class SetDelegate<T> implements Delegate<Set<T>> {
private final Schema<T> elementSchema;
public SetDelegate(Schema<T> elementSchema) {
this.elementSchema = elementSchema;
}
@Override
public FieldType getFieldType() {
return FieldType.MESSAGE;
}
@Override
public Set<T> readFrom(Input input) throws IOException {
Set<T> set = new LinkedHashSet<>();
// Read count
int count = input.readInt32();
// Read elements
for (int i = 0; i < count; i++) {
T element = elementSchema.newMessage();
input.mergeObject(element, elementSchema);
set.add(element);
}
return set;
}
@Override
public void writeTo(Output output, int number, Set<T> value, boolean repeated) throws IOException {
if (value != null && !value.isEmpty()) {
output.writeObject(number, value, this, repeated);
}
}
@Override
public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated)
throws IOException {
// Implementation for pipe transfer
output.writeObject(number, pipe, this, repeated);
}
@Override
public Class<?> typeClass() {
return Set.class;
}
}import io.protostuff.runtime.DefaultIdStrategy;
// Create ID strategy
DefaultIdStrategy strategy = new DefaultIdStrategy();
// Register various delegates
strategy.registerDelegate(new DateDelegate());
strategy.registerDelegate(new CompressedStringDelegate());
strategy.registerDelegate(new SafeEnumDelegate<>(Status.class, Status.UNKNOWN));
// Register by class name (useful for optional dependencies)
strategy.registerDelegate("java.time.LocalDateTime", new LocalDateTimeDelegate());
strategy.registerDelegate("java.math.BigDecimal", new BigDecimalDelegate());
// Use strategy with schema
Schema<MyClass> schema = RuntimeSchema.getSchema(MyClass.class, strategy);/**
* Delegate that chooses serialization strategy based on value characteristics
*/
public class OptimizedStringDelegate implements Delegate<String> {
private static final int COMPRESSION_THRESHOLD = 100;
@Override
public FieldType getFieldType() {
return FieldType.BYTES;
}
@Override
public String readFrom(Input input) throws IOException {
byte[] data = input.readByteArray();
// First byte indicates compression
if (data[0] == 1) {
return decompress(Arrays.copyOfRange(data, 1, data.length));
} else {
return new String(Arrays.copyOfRange(data, 1, data.length));
}
}
@Override
public void writeTo(Output output, int number, String value, boolean repeated) throws IOException {
if (value != null) {
byte[] data;
// Compress long strings
if (value.length() > COMPRESSION_THRESHOLD) {
byte[] compressed = compress(value);
data = new byte[compressed.length + 1];
data[0] = 1; // Compression flag
System.arraycopy(compressed, 0, data, 1, compressed.length);
} else {
byte[] raw = value.getBytes();
data = new byte[raw.length + 1];
data[0] = 0; // No compression flag
System.arraycopy(raw, 0, data, 1, raw.length);
}
output.writeByteArray(number, data, repeated);
}
}
// ... other methods
}/**
* Delegate supporting multiple versions of serialization format
*/
public class VersionedUserDelegate implements Delegate<User> {
private static final int CURRENT_VERSION = 2;
@Override
public FieldType getFieldType() {
return FieldType.MESSAGE;
}
@Override
public User readFrom(Input input) throws IOException {
int version = input.readInt32();
switch (version) {
case 1:
return readV1(input);
case 2:
return readV2(input);
default:
throw new IOException("Unsupported version: " + version);
}
}
@Override
public void writeTo(Output output, int number, User value, boolean repeated) throws IOException {
if (value != null) {
// Always write current version
output.writeInt32(1, CURRENT_VERSION, false);
writeV2(output, value);
}
}
private User readV1(Input input) throws IOException {
// Legacy format handling
User user = new User();
user.setName(input.readString());
user.setAge(input.readInt32());
return user;
}
private User readV2(Input input) throws IOException {
// Current format handling
User user = new User();
user.setName(input.readString());
user.setAge(input.readInt32());
user.setEmail(input.readString());
return user;
}
private void writeV2(Output output, User user) throws IOException {
output.writeString(2, user.getName(), false);
output.writeInt32(3, user.getAge(), false);
output.writeString(4, user.getEmail(), false);
}
// ... other methods
}/**
* Wrapper delegate that handles null values explicitly
*/
public class NullableDelegate<T> implements Delegate<T> {
private final Delegate<T> innerDelegate;
public NullableDelegate(Delegate<T> innerDelegate) {
this.innerDelegate = innerDelegate;
}
@Override
public FieldType getFieldType() {
return FieldType.MESSAGE;
}
@Override
public T readFrom(Input input) throws IOException {
boolean isNull = input.readBool();
if (isNull) {
return null;
}
return innerDelegate.readFrom(input);
}
@Override
public void writeTo(Output output, int number, T value, boolean repeated) throws IOException {
output.writeBool(1, value == null, false);
if (value != null) {
innerDelegate.writeTo(output, 2, value, false);
}
}
@Override
public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated)
throws IOException {
boolean isNull = input.readBool();
output.writeBool(1, isNull, false);
if (!isNull) {
innerDelegate.transfer(pipe, input, output, 2, false);
}
}
@Override
public Class<?> typeClass() {
return innerDelegate.typeClass();
}
}FieldType for efficient wire formatwriteTo methodtransfer method for efficient pipe operationsInstall with Tessl CLI
npx tessl i tessl/maven-io-protostuff--protostuff-runtime