What is the best approach for serializing BigDecimal/BigInteger to ProtocolBuffers

Yes. You should define BigInteger as BigInteger.toByteArray() .

My guess is that BigDecimal would be:


message BDecimal {
  required int32 scale = 1;
  required BInteger int_val = 2;
}

while BigInteger may be defined as


message BInteger {
  required bytes value = 1;
}

The code to handle BigInteger would be:


  BInteger write(BigInteger val) {
    BInteger.Builder builder = BInteger.newBuilder();
    ByteString bytes = ByteString.copyFrom(val.toByteArray());
    builder.setValue(bytes);
    return builder.build();
  }

  BigInteger read(BInteger message) {
    ByteString bytes = message.getValue();
    return new BigInteger(bytes.toByteArray());
  }

I have recently had the same need as OP, and used a similar solution to the one proposed by @notnoop. Posting it here after seeing this comment from @stikkos:

How would you convert a BigDecimal to a BigInteger and scale? And back ?

According to the BigDecimal class documentation:

The value of the BigDecimal is (unscaledVal × 10-scale), rounded according to the precision and rounding mode settings.

So, a java.math.BigDecimal can be serialized considering three properties:

  • unscaledValue
  • scale
  • precision

Now, the code.

  1. Protobuf v3 message definition:

    syntax = "proto3";
    
    message DecimalValue {
      uint32 scale = 1;
      uint32 precision = 2;
      bytes value = 3;
    }
    
  2. How to serialize a java.math.BigDecimal to a Protobuf message:

    import com.google.protobuf.ByteString;
    import DecimalValue;
    
    java.math.BigDecimal bigDecimal = new java.math.BigDecimal("1234.56789");
    DecimalValue serialized = DecimalValue.newBuilder()
            .setScale(bigDecimal.scale())
            .setPrecision(bigDecimal.precision())
            .setValue(ByteString.copyFrom(bigDecimal.unscaledValue().toByteArray()))
            .build();
    
  3. How to deserialize the Protobuf message back to a java.math.BigDecimal:

    java.math.MathContext mc = new java.math.MathContext(serialized.getPrecision());
    java.math.BigDecimal deserialized = new java.math.BigDecimal(
            new java.math.BigInteger(serialized.getValue().toByteArray()),
            serialized.getScale(),
            mc);
    
  4. We can check the obtained java.math.BigDecimal yields the same original value as follows:

    assert bigDecimal.equals(deserialized);
    

Note: OP assumed there is a better way to serialize a java.math.BigDecimal than using a plain String. In terms of storage, this solution is better.

For example, to serialize the first 100 decimals of π:

3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679
System.out.println("DecimalValue: " + serialized.getSerializedSize());
System.out.println("String: " + StringValue.of(pi).getSerializedSize());

This yields:

DecimalValue: 48
String: 104