Skip to content

Commit 5dafa5b

Browse files
committed
Add support for nullable numbers with default values (mimics HLRC)
1 parent 30642b0 commit 5dafa5b

File tree

6 files changed

+159
-2
lines changed

6 files changed

+159
-2
lines changed

java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ static JsonpDeserializer<Double> doubleDeserializer() {
181181
return JsonpDeserializerBase.DOUBLE;
182182
}
183183

184+
/** A {@code double} deserializer that will return a default value when the JSON value is {@code null} */
185+
static JsonpDeserializer<Double> doubleOrNullDeserializer(double defaultValue) {
186+
return new JsonpDeserializerBase.DoubleOrNullDeserializer(defaultValue);
187+
}
188+
189+
/** An {@code integer} deserializer that will return a default value when the JSON value is {@code null} */
190+
static JsonpDeserializer<Integer> intOrNullDeserializer(int defaultValue) {
191+
return new JsonpDeserializerBase.IntOrNullDeserializer(defaultValue);
192+
}
193+
184194
static JsonpDeserializer<Number> numberDeserializer() {
185195
return JsonpDeserializerBase.NUMBER;
186196
}

java-client/src/main/java/co/elastic/clients/json/JsonpDeserializerBase.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,67 @@ public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
181181
}
182182
};
183183

184+
static final class DoubleOrNullDeserializer extends JsonpDeserializerBase<Double> {
185+
static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL);
186+
static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_NULL);
187+
private final double defaultValue;
188+
189+
DoubleOrNullDeserializer(double defaultValue) {
190+
super(acceptedEvents, nativeEvents);
191+
this.defaultValue = defaultValue;
192+
}
193+
194+
@Override
195+
public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
196+
if (event == Event.VALUE_NULL) {
197+
return defaultValue;
198+
}
199+
if (event == Event.VALUE_STRING) {
200+
return Double.valueOf(parser.getString());
201+
}
202+
return parser.getBigDecimal().doubleValue();
203+
}
204+
}
205+
206+
static final class IntOrNullDeserializer extends JsonpDeserializerBase<Integer> {
207+
static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL);
208+
static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_NULL);
209+
private final int defaultValue;
210+
211+
IntOrNullDeserializer(int defaultValue) {
212+
super(acceptedEvents, nativeEvents);
213+
this.defaultValue = defaultValue;
214+
}
215+
216+
@Override
217+
public Integer deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
218+
if (event == Event.VALUE_NULL) {
219+
return defaultValue;
220+
}
221+
if (event == Event.VALUE_STRING) {
222+
return Integer.valueOf(parser.getString());
223+
}
224+
return parser.getInt();
225+
}
226+
}
227+
228+
static final JsonpDeserializer<Double> DOUBLE_OR_NAN =
229+
new JsonpDeserializerBase<Double>(
230+
EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING, Event.VALUE_NULL),
231+
EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL)
232+
) {
233+
@Override
234+
public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
235+
if (event == Event.VALUE_NULL) {
236+
return Double.NaN;
237+
}
238+
if (event == Event.VALUE_STRING) {
239+
return Double.valueOf(parser.getString());
240+
}
241+
return parser.getBigDecimal().doubleValue();
242+
}
243+
};
244+
184245
static final JsonpDeserializer<Number> NUMBER =
185246
new JsonpDeserializerBase<Number>(
186247
EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),

java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,22 @@ public static String toString(JsonValue value) {
180180
throw new IllegalArgumentException("Unknown JSON value type: '" + value + "'");
181181
}
182182
}
183+
184+
public static void serializeDoubleOrNull(JsonGenerator generator, double value, double defaultValue) {
185+
// Only output null if the default value isn't finite, which cannot be represented as JSON
186+
if (value == defaultValue && !Double.isFinite(defaultValue)) {
187+
generator.writeNull();
188+
} else {
189+
generator.write(value);
190+
}
191+
}
192+
193+
public static void serializeIntOrNull(JsonGenerator generator, int value, int defaultValue) {
194+
// Only output null if the default value isn't finite, which cannot be represented as JSON
195+
if (value == defaultValue && defaultValue == Integer.MAX_VALUE || defaultValue == Integer.MIN_VALUE) {
196+
generator.writeNull();
197+
} else {
198+
generator.write(value);
199+
}
200+
}
183201
}

java-client/src/main/java/co/elastic/clients/util/ApiTypeHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public static <T> List<T> undefinedList() {
116116
* <p>
117117
* List setters on API type builders add items to the property using {@link List#addAll(Collection)} and do not accept
118118
* a {@code null} value which is considered as a bug. In some rare occasions it may however be necessary to reset
119-
* the builder's property, and this what the result of this method is meant for: setting a builder's list property
119+
* the builder's property, and this is what the result of this method is meant for: setting a builder's list property
120120
* to the value returned by this method will reset that property to {@code null}.
121121
*/
122122
@SuppressWarnings("unchecked")
@@ -183,7 +183,7 @@ public static <K, V> Map<K, V> undefinedMap() {
183183
* <p>
184184
* Map setters on API type builders add entries to the property using {@link Map#putAll(Map)} and do not accept
185185
* a {@code null} value which is considered as a bug. In some rare occasions it may however be necessary to reset
186-
* the builder's property, and this what the result of this method is meant for: setting a builder's map property
186+
* the builder's property, and this is what the result of this method is meant for: setting a builder's map property
187187
* to the value returned by this method will reset that property to {@code null}.
188188
*/
189189
@SuppressWarnings("unchecked")

java-client/src/test/java/co/elastic/clients/elasticsearch/end_to_end/RequestTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ public void testDataIngestion() throws Exception {
168168
assertEquals(1337, esData.getIntValue());
169169
assertEquals("foo", esData.getMsg());
170170

171+
// Query by id a non-existing document
172+
final GetResponse<AppData> notExists = client.get(b -> b
173+
.index(index)
174+
.id("some-random-id")
175+
, AppData.class
176+
);
177+
178+
assertFalse(notExists.found());
179+
assertNull(notExists.source());
180+
171181
// Search
172182
SearchResponse<AppData> search = client.search(b -> b
173183
.index(index)

java-client/src/test/java/co/elastic/clients/elasticsearch/model/BuiltinTypesTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import co.elastic.clients.elasticsearch._types.FieldValue;
2323
import co.elastic.clients.elasticsearch._types.SortOptions;
2424
import co.elastic.clients.elasticsearch._types.SortOrder;
25+
import co.elastic.clients.elasticsearch._types.aggregations.StatsAggregate;
26+
import co.elastic.clients.elasticsearch._types.aggregations.StringStatsAggregate;
2527
import co.elastic.clients.elasticsearch._types.query_dsl.SpanGapQuery;
2628
import co.elastic.clients.elasticsearch.core.SearchRequest;
2729
import co.elastic.clients.elasticsearch.indices.IndexSettings;
@@ -159,4 +161,60 @@ public void testFieldValue() {
159161
assertEquals("foo", f._toJsonString());
160162

161163
}
164+
165+
@Test
166+
public void testNullableDouble() {
167+
StatsAggregate stats;
168+
169+
// Regular values
170+
stats = StatsAggregate.statsAggregateOf(b -> b // Parent classes can't have an overloaded "of" method
171+
.count(10)
172+
.min(1.0)
173+
.avg(1.5)
174+
.max(2.0)
175+
.sum(5.0)
176+
);
177+
178+
stats = checkJsonRoundtrip(stats, "{\"count\":10,\"min\":1.0,\"max\":2.0,\"avg\":1.5,\"sum\":5.0}");
179+
assertEquals(10, stats.count());
180+
assertEquals(1.0, stats.min(), 0.01);
181+
assertEquals(1.5, stats.avg(), 0.01);
182+
assertEquals(2.0, stats.max(), 0.01);
183+
assertEquals(5.0, stats.sum(), 0.01);
184+
185+
// Missing values (JSON null, Java infinite)
186+
String json = "{\"count\":0,\"min\":null,\"max\":null,\"avg\":null,\"sum\":0.0}";
187+
stats = fromJson(json, StatsAggregate.class);
188+
189+
assertEquals(0, stats.count());
190+
assertTrue(Double.isInfinite(stats.min()));
191+
assertEquals(0.0, stats.avg(), 0.01);
192+
assertTrue(Double.isInfinite(stats.max()));
193+
assertEquals(0.0, stats.sum(), 0.01);
194+
195+
// We don't serialize finite default values as null
196+
assertEquals("{\"count\":0,\"min\":null,\"max\":null,\"avg\":0.0,\"sum\":0.0}", toJson(stats));
197+
}
198+
199+
@Test
200+
public void testNullableInt() {
201+
StringStatsAggregate stats = StringStatsAggregate.of(b -> b
202+
.count(1)
203+
.minLength(2)
204+
.avgLength(3)
205+
.maxLength(4)
206+
.entropy(0)
207+
);
208+
209+
stats = checkJsonRoundtrip(stats, "{\"count\":1,\"min_length\":2,\"max_length\":4,\"avg_length\":3.0,\"entropy\":0.0}");
210+
assertEquals(2, stats.minLength());
211+
assertEquals(4, stats.maxLength());
212+
213+
// Missing values
214+
String json = "{\"count\":1,\"min_length\":null,\"max_length\":null,\"avg_length\":null,\"entropy\":null}";
215+
stats = fromJson(json, StringStatsAggregate.class);
216+
assertEquals(0, stats.minLength());
217+
assertEquals(0, stats.maxLength());
218+
assertEquals(0.0, stats.entropy(), 0.01);
219+
}
162220
}

0 commit comments

Comments
 (0)