Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bug Fixes

- Accept both JSON numbers and decimal strings when deserializing int64 (`Long`) response fields.

### Security Vulnerabilities

### Documentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.fasterxml.jackson.databind.type.LogicalType;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
Expand All @@ -26,6 +29,13 @@ public static ObjectMapper createMapper() {
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);

// Databricks services return int64 fields as either JSON numbers (123) or decimal strings
// ("123"). Pin String->integer coercion so both deserialize into Long. Jackson does this by
// default; pinning it guards against a silent regression if that default is ever tightened.
mapper
.coercionConfigFor(LogicalType.Integer)
.setCoercion(CoercionInputShape.String, CoercionAction.TryConvert);
return mapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.databricks.sdk.core.utils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;

/**
* Verifies that int64 ({@code Long}) fields deserialize from both JSON numbers ({@code 123}) and
* decimal strings ({@code "123"}), which Databricks services use interchangeably. The behavior is
* pinned in {@link SerDeUtils#createMapper()}; this mirrors the Go SDK's {@code
* marshal/int64_test.go}.
*/
public class SerDeUtilsInt64Test {
private final ObjectMapper mapper = SerDeUtils.createMapper();

public static class LongHolder {
public Long value;
}

public static class Inner {
public Long id;
}

public static class Outer {
public Inner inner;
}

public static class ListHolder {
public List<Long> values;
}

public static class MapHolder {
public Map<String, Long> counts;
}

public static class Mixed {
public Long id;
public String name;
}

public static class UntypedHolder {
public Object payload;
}

@Test
public void topLevelLongFromNumberAndString() throws JsonProcessingException {
assertEquals(Long.valueOf(123L), mapper.readValue("123", Long.class));
assertEquals(Long.valueOf(123L), mapper.readValue("\"123\"", Long.class));
}

@Test
public void longFieldFromNumberAndString() throws JsonProcessingException {
assertEquals(Long.valueOf(123L), mapper.readValue("{\"value\":123}", LongHolder.class).value);
assertEquals(
Long.valueOf(123L), mapper.readValue("{\"value\":\"123\"}", LongHolder.class).value);
}

@Test
public void nestedLongFromString() throws JsonProcessingException {
Outer out = mapper.readValue("{\"inner\":{\"id\":\"456\"}}", Outer.class);
assertEquals(Long.valueOf(456L), out.inner.id);
}

@Test
public void listOfLongAcceptsMixedForms() throws JsonProcessingException {
ListHolder h = mapper.readValue("{\"values\":[\"1\",2,\"3\"]}", ListHolder.class);
assertEquals(Arrays.asList(1L, 2L, 3L), h.values);
}

@Test
public void mapOfLongAcceptsMixedForms() throws JsonProcessingException {
MapHolder h = mapper.readValue("{\"counts\":{\"a\":\"1\",\"b\":2}}", MapHolder.class);
assertEquals(Long.valueOf(1L), h.counts.get("a"));
assertEquals(Long.valueOf(2L), h.counts.get("b"));
}

@Test
public void boundaryValuesFromNumberAndString() throws JsonProcessingException {
assertEquals(
Long.valueOf(Long.MAX_VALUE),
mapper.readValue("{\"value\":9223372036854775807}", LongHolder.class).value);
assertEquals(
Long.valueOf(Long.MAX_VALUE),
mapper.readValue("{\"value\":\"9223372036854775807\"}", LongHolder.class).value);
assertEquals(
Long.valueOf(Long.MIN_VALUE),
mapper.readValue("{\"value\":-9223372036854775808}", LongHolder.class).value);
assertEquals(
Long.valueOf(Long.MIN_VALUE),
mapper.readValue("{\"value\":\"-9223372036854775808\"}", LongHolder.class).value);
}

@Test
public void invalidNumericStringFails() {
assertThrows(
JsonProcessingException.class,
() -> mapper.readValue("{\"value\":\"not-an-int\"}", LongHolder.class));
}

@Test
public void overflowStringFails() {
// 20 nines: larger than Long.MAX_VALUE.
assertThrows(
JsonProcessingException.class,
() -> mapper.readValue("{\"value\":\"99999999999999999999\"}", LongHolder.class));
}

@Test
public void stringFieldIsNotCoercedToNumber() throws JsonProcessingException {
Mixed m = mapper.readValue("{\"id\":\"5\",\"name\":\"5\"}", Mixed.class);
assertEquals(Long.valueOf(5L), m.id);
assertEquals("5", m.name);
}

@Test
public void untypedFieldPreservesJsonForm() throws JsonProcessingException {
// Coercion only targets integer-typed fields, so an untyped field keeps its JSON form.
UntypedHolder asString = mapper.readValue("{\"payload\":\"123\"}", UntypedHolder.class);
assertInstanceOf(String.class, asString.payload);
assertEquals("123", asString.payload);

UntypedHolder asNumber = mapper.readValue("{\"payload\":123}", UntypedHolder.class);
assertInstanceOf(Number.class, asNumber.payload);
}
}
Loading