Skip to content

Commit abe7a78

Browse files
authored
Implement #5242: support for "packed binary vectors" (for float[], double[]) (#5249)
1 parent 1c1c1b8 commit abe7a78

File tree

6 files changed

+529
-20
lines changed

6 files changed

+529
-20
lines changed

release-notes/VERSION-2.x

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Project: jackson-databind
44
=== Releases ===
55
------------------------------------------------------------------------
66

7+
Not yet released
8+
9+
#5242: Support "binary vectors": `@JsonFormat(shape = Shape.BINARY)` for
10+
`float[]`, `double[]`
11+
712
2.20.0-rc1 (04-Aug-2025)
813

914
#3072: Allow specifying `@JacksonInject` does not fail when there's no

src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ public T deserialize(JsonParser p, DeserializationContext ctxt, T existing) thro
213213
@SuppressWarnings("unchecked")
214214
protected T handleNonArray(JsonParser p, DeserializationContext ctxt) throws IOException
215215
{
216-
217216
final boolean canWrap = (_unwrapSingle == Boolean.TRUE) ||
218217
((_unwrapSingle == null) &&
219218
ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));
@@ -269,10 +268,8 @@ protected char[] _constructEmpty() {
269268
@Override
270269
public char[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
271270
{
272-
/* Won't take arrays, must get a String (could also
273-
* convert other tokens to Strings... but let's not bother
274-
* yet, doesn't seem to make sense)
275-
*/
271+
// Won't take arrays, must get a String (could also convert other tokens to Strings...
272+
// but let's not bother yet, doesn't seem to make sense)
276273
if (p.hasToken(JsonToken.VALUE_STRING)) {
277274
// note: can NOT return shared internal buffer, must copy:
278275
char[] buffer = p.getTextCharacters();
@@ -483,6 +480,7 @@ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
483480
return (byte[]) ctxt.handleWeirdStringValue(byte[].class,
484481
p.getText(), msg);
485482
}
483+
throw e;
486484
}
487485
}
488486
// 31-Dec-2009, tatu: Also may be hidden as embedded Object
@@ -821,6 +819,10 @@ protected float[] _constructEmpty() {
821819
public float[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
822820
{
823821
if (!p.isExpectedStartArrayToken()) {
822+
float[] decoded = _deserializeBinaryVector(p, ctxt);
823+
if (decoded != null) {
824+
return decoded;
825+
}
824826
return handleNonArray(p, ctxt);
825827
}
826828
ArrayBuilders.FloatBuilder builder = ctxt.getArrayBuilders().getFloatBuilder();
@@ -864,6 +866,61 @@ protected float[] _concat(float[] oldValue, float[] newValue) {
864866
System.arraycopy(newValue, 0, result, len1, len2);
865867
return result;
866868
}
869+
870+
private float[] _deserializeBinaryVector(JsonParser p, DeserializationContext ctxt)
871+
throws IOException
872+
{
873+
JsonToken t = p.currentToken();
874+
byte[] packed = null;
875+
876+
// Typical textual format case: base64 encoded String (for Packed Binary Vector)
877+
if (t == JsonToken.VALUE_STRING) {
878+
try {
879+
packed = p.getBinaryValue(ctxt.getBase64Variant());
880+
} catch (StreamReadException | DatabindException e) {
881+
// [databind#1425], try to convert to a more usable one, as it's not really
882+
// a JSON-level parse exception, but rather binding from JSON String into
883+
// base64 decoded binary data
884+
String msg = e.getOriginalMessage();
885+
if (msg.contains("base64")) {
886+
return (float[]) ctxt.handleWeirdStringValue(float[].class,
887+
p.getText(), msg);
888+
}
889+
throw e;
890+
}
891+
} else if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
892+
// Typical for binary formats
893+
Object ob = p.getEmbeddedObject();
894+
if (ob instanceof byte[]) {
895+
packed = (byte[]) ob;
896+
} else if (ob == null || (ob instanceof float[])) {
897+
return (float[]) ob;
898+
}
899+
}
900+
// Packed Binary Vector case
901+
if (packed != null) {
902+
return _unpack(ctxt, packed);
903+
}
904+
return null;
905+
}
906+
907+
private float[] _unpack(DeserializationContext ctxt, byte[] bytes) throws IOException {
908+
final int bytesLen = bytes.length;
909+
if ((bytesLen & 3) != 0) {
910+
return (float[]) ctxt.reportInputMismatch(handledType(),
911+
"Vector length for Packed Binary Float Vector (%d) not a multiple of 4 bytes", bytesLen);
912+
}
913+
final int vectorLen = bytesLen >> 2;
914+
final float[] floats = new float[vectorLen];
915+
for (int in = 0, out = 0; in < bytesLen; ) {
916+
int packed = (bytes[in++] << 24)
917+
| ((bytes[in++] & 0xFF) << 16)
918+
| ((bytes[in++] & 0xFF) << 8)
919+
| (bytes[in++] & 0xFF);
920+
floats[out++] = Float.intBitsToFloat(packed);
921+
}
922+
return floats;
923+
}
867924
}
868925

869926
@JacksonStdImpl
@@ -892,6 +949,10 @@ protected double[] _constructEmpty() {
892949
public double[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
893950
{
894951
if (!p.isExpectedStartArrayToken()) {
952+
double[] decoded = _deserializeBinaryVector(p, ctxt);
953+
if (decoded != null) {
954+
return decoded;
955+
}
895956
return handleNonArray(p, ctxt);
896957
}
897958
ArrayBuilders.DoubleBuilder builder = ctxt.getArrayBuilders().getDoubleBuilder();
@@ -934,5 +995,65 @@ protected double[] _concat(double[] oldValue, double[] newValue) {
934995
System.arraycopy(newValue, 0, result, len1, len2);
935996
return result;
936997
}
998+
999+
private double[] _deserializeBinaryVector(JsonParser p, DeserializationContext ctxt)
1000+
throws IOException
1001+
{
1002+
JsonToken t = p.currentToken();
1003+
byte[] packed = null;
1004+
1005+
// Typical textual format case: base64 encoded String (for Packed Binary Vector)
1006+
if (t == JsonToken.VALUE_STRING) {
1007+
try {
1008+
packed = p.getBinaryValue(ctxt.getBase64Variant());
1009+
} catch (StreamReadException | DatabindException e) {
1010+
// [databind#1425], try to convert to a more usable one, as it's not really
1011+
// a JSON-level parse exception, but rather binding from JSON String into
1012+
// base64 decoded binary data
1013+
String msg = e.getOriginalMessage();
1014+
if (msg.contains("base64")) {
1015+
return (double[]) ctxt.handleWeirdStringValue(double[].class,
1016+
p.getText(), msg);
1017+
}
1018+
throw e;
1019+
}
1020+
} else if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
1021+
// Typical for binary formats
1022+
Object ob = p.getEmbeddedObject();
1023+
if (ob instanceof byte[]) {
1024+
packed = (byte[]) ob;
1025+
} else if (ob == null || (ob instanceof double[])) {
1026+
return (double[]) ob;
1027+
}
1028+
}
1029+
// Packed Binary Vector case
1030+
if (packed != null) {
1031+
return _unpack(ctxt, packed);
1032+
}
1033+
return null;
1034+
}
1035+
1036+
private double[] _unpack(DeserializationContext ctxt, byte[] bytes) throws IOException {
1037+
final int bytesLen = bytes.length;
1038+
if ((bytesLen & 7) != 0) {
1039+
return (double[]) ctxt.reportInputMismatch(handledType(),
1040+
"Vector length for Packed Binary Double Vector (%d) not a multiple of 8 bytes", bytesLen);
1041+
}
1042+
final int vectorLen = bytesLen >> 3;
1043+
final double[] doubles = new double[vectorLen];
1044+
for (int in = 0, out = 0; in < bytesLen; ) {
1045+
int packed1 = (bytes[in++] << 24)
1046+
| ((bytes[in++] & 0xFF) << 16)
1047+
| ((bytes[in++] & 0xFF) << 8)
1048+
| (bytes[in++] & 0xFF);
1049+
int packed2 = (bytes[in++] << 24)
1050+
| ((bytes[in++] & 0xFF) << 16)
1051+
| ((bytes[in++] & 0xFF) << 8)
1052+
| (bytes[in++] & 0xFF);
1053+
long packed = ((long) packed1 << 32) | (packed2 & 0xFFFFFFFFL);
1054+
doubles[out++] = Double.longBitsToDouble(packed);
1055+
}
1056+
return doubles;
1057+
}
9371058
}
9381059
}

0 commit comments

Comments
 (0)