As of version 0.2.1, OJP uses a compact, language-neutral wire format for serializing BigDecimal values between the JDBC driver and server, replacing Java object serialization.
The wire format for BigDecimal is binary and uses big-endian byte order:
-
Presence Flag (1 byte)
0= null value1= non-null value
-
If non-null, the following fields are present:
- Unscaled Value Length (4 bytes, signed int32, big-endian)
- Length of the UTF-8 encoded string representation of the unscaled value
- Unscaled Value (variable length bytes)
- UTF-8 encoding of the BigInteger unscaled value as a decimal string
- Example:
"-12345678901234567890"for a large negative number
- Scale (4 bytes, signed int32, big-endian)
- The scale of the BigDecimal (number of digits to the right of the decimal point)
- Can be negative, zero, or positive
- Unscaled Value Length (4 bytes, signed int32, big-endian)
For BigDecimal("123.45"):
- Presence flag:
0x01(1 byte) - Unscaled value:
"12345" - Unscaled value length:
0x00000005(5 in big-endian int32) - Unscaled value bytes:
0x3132333435(UTF-8 bytes for "12345") - Scale:
0x00000002(2 in big-endian int32)
The BigDecimalWire utility class provides serialization methods:
import org.openjproxy.grpc.BigDecimalWire;
import java.io.DataOutputStream;
import java.io.DataInputStream;
import java.math.BigDecimal;
// Writing
DataOutputStream out = ...;
BigDecimal value = new BigDecimal("123.45");
BigDecimalWire.writeBigDecimal(out, value);
// Reading
DataInputStream in = ...;
BigDecimal value = BigDecimalWire.readBigDecimal(in);import struct
from decimal import Decimal
def write_bigdecimal(output, value):
if value is None:
output.write(b'\x00')
return
output.write(b'\x01')
# Convert Decimal to unscaled value and scale
sign, digits, exponent = value.as_tuple()
# Concatenate digits to form unscaled integer
unscaled_str = ''.join(map(str, digits))
if sign: # sign is 1 for negative
unscaled_str = '-' + unscaled_str
unscaled_bytes = unscaled_str.encode('utf-8')
output.write(struct.pack('>i', len(unscaled_bytes)))
output.write(unscaled_bytes)
# Scale is negative of exponent
scale = -exponent
output.write(struct.pack('>i', scale))
def read_bigdecimal(input):
present = input.read(1)[0]
if present == 0:
return None
length = struct.unpack('>i', input.read(4))[0]
unscaled_bytes = input.read(length)
unscaled_str = unscaled_bytes.decode('utf-8')
scale = struct.unpack('>i', input.read(4))[0]
# Reconstruct Decimal from unscaled value and scale
unscaled = int(unscaled_str)
# Create Decimal with negative exponent (scale)
return Decimal(unscaled) / Decimal(10 ** scale)Note: Go's standard library doesn't have a built-in BigDecimal type, so this example uses a simplified approach with big.Int for the unscaled value and tracking scale separately. For production use, consider using a third-party decimal library like shopspring/decimal.
import (
"encoding/binary"
"fmt"
"io"
"math/big"
)
// BigDecimalValue represents a decimal value with unscaled integer and scale
type BigDecimalValue struct {
Unscaled *big.Int
Scale int32
}
func WriteBigDecimal(w io.Writer, value *BigDecimalValue) error {
if value == nil {
return binary.Write(w, binary.BigEndian, byte(0))
}
if err := binary.Write(w, binary.BigEndian, byte(1)); err != nil {
return err
}
// Convert unscaled value to decimal string
unscaledStr := value.Unscaled.String()
unscaledBytes := []byte(unscaledStr)
if err := binary.Write(w, binary.BigEndian, int32(len(unscaledBytes))); err != nil {
return err
}
if _, err := w.Write(unscaledBytes); err != nil {
return err
}
return binary.Write(w, binary.BigEndian, value.Scale)
}
func ReadBigDecimal(r io.Reader) (*BigDecimalValue, error) {
var present byte
if err := binary.Read(r, binary.BigEndian, &present); err != nil {
return nil, err
}
if present == 0 {
return nil, nil
}
var length int32
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return nil, err
}
unscaledBytes := make([]byte, length)
if _, err := io.ReadFull(r, unscaledBytes); err != nil {
return nil, err
}
var scale int32
if err := binary.Read(r, binary.BigEndian, &scale); err != nil {
return nil, err
}
// Parse unscaled value
unscaled := new(big.Int)
if _, ok := unscaled.SetString(string(unscaledBytes), 10); !ok {
return nil, fmt.Errorf("invalid unscaled value: %s", string(unscaledBytes))
}
return &BigDecimalValue{
Unscaled: unscaled,
Scale: scale,
}, nil
}
// ToFloat64 converts BigDecimalValue to float64 (may lose precision)
func (bd *BigDecimalValue) ToFloat64() float64 {
f := new(big.Float).SetInt(bd.Unscaled)
divisor := new(big.Float).SetInt(
new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(bd.Scale)), nil))
result, _ := new(big.Float).Quo(f, divisor).Float64()
return result
}- Language Neutrality: The string-based format can be parsed in any language
- Simplicity: No need to understand Java's serialization protocol
- Safety: No risk of deserialization vulnerabilities
- Efficiency: More compact than Java serialization for most values
- Transparency: Easy to debug and inspect the wire format
While sending the raw bytes of BigInteger.toByteArray() would be more compact, the string representation:
- Is easier to parse in languages without native BigInteger support
- Is human-readable during debugging
- Avoids issues with sign bit representation across languages
- Simplifies implementation in multiple languages
The implementation includes safeguards:
- Maximum length validation (default: 10,000,000 bytes) prevents DOS attacks
- Negative length validation prevents buffer overflows
- UTF-8 encoding is explicit and validated
This is a breaking change - old serialized BigDecimal values cannot be read with the new format.
Since this affects the driver-server protocol:
- Deploy updated server and driver together in a coordinated release
- No backward compatibility with pre-0.2.1 versions for BigDecimal data
Future versions may add protocol version negotiation. For now, ensure both driver and server are updated together.
The new format is generally:
- Faster for small to medium BigDecimal values
- More compact than Java serialization for most common cases
- Comparable for very large values
Benchmark results show 2-3x improvement in serialization/deserialization speed for typical decimal values.