Skip to content

Commit 051da99

Browse files
Allow serializing aggregations without typed keys (#316) (#325)
Co-authored-by: Sylvain Wallez <[email protected]>
1 parent 7735ed4 commit 051da99

File tree

12 files changed

+436
-61
lines changed

12 files changed

+436
-61
lines changed

docs/troubleshooting/index.asciidoc

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
.Exceptions
88

99
* <<missing-required-property>>
10+
* <<serialize-without-typed-keys>>
1011

1112

1213
// [[debugging]]
@@ -16,3 +17,4 @@
1617
// === Elasticsearch deprecation warnings
1718

1819
include::missing-required-property.asciidoc[]
20+
include::serialize-without-typed-keys.asciidoc[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[[serialize-without-typed-keys]]
2+
=== Serializing aggregations and suggestions without typed keys
3+
4+
{es} search requests accept a `typed_key` parameter that allow returning type information along with the name in aggregation and suggestion results (see the {es-docs}/search-aggregations.html#return-agg-type[aggregations documentation] for additional details).
5+
6+
The {java-client} always adds this parameter to search requests, as type information is needed to know the concrete class that should be used to deserialize aggregation and suggestion results.
7+
8+
Symmetrically, the {java-client} also serializes aggregation and suggestion results using this `typed_keys` format, so that it can correctly deserialize the results of its own serialization.
9+
10+
["source","java"]
11+
--------------------------------------------------
12+
ElasticsearchClient esClient = ...
13+
include-tagged::{doc-tests-src}/troubleshooting/TroubleShootingTests.java[aggregation-typed-keys]
14+
--------------------------------------------------
15+
16+
However, in some use cases serializing objects in the `typed_keys` format may not be desirable, for example when the {java-client} is used in an application that acts as a front-end to other services that expect the default format for aggregations and suggestions.
17+
18+
You can disable `typed_keys` serialization by setting the `JsonpMapperFeatures.SERIALIZE_TYPED_KEYS` attribute to `false` on your mapper object:
19+
20+
["source","java"]
21+
--------------------------------------------------
22+
ElasticsearchClient esClient = ...
23+
include-tagged::{doc-tests-src}/troubleshooting/TroubleShootingTests.java[aggregation-no-typed-keys]
24+
--------------------------------------------------

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

+48-16
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ public void deserializeEntry(String key, JsonParser parser, JsonpMapper mapper,
153153
}
154154

155155
/**
156-
* Serialize an externally tagged union using the typed keys encoding.
156+
* Serialize a map of externally tagged union objects.
157+
* <p>
158+
* If {@link JsonpMapperFeatures#SERIALIZE_TYPED_KEYS} is <code>true</code> (the default), the typed keys encoding
159+
* (<code>type#name</code>) is used.
157160
*/
158161
static <T extends JsonpSerializable & TaggedUnion<? extends JsonEnum, ?>> void serializeTypedKeys(
159162
Map<String, T> map, JsonGenerator generator, JsonpMapper mapper
@@ -163,36 +166,65 @@ public void deserializeEntry(String key, JsonParser parser, JsonpMapper mapper,
163166
generator.writeEnd();
164167
}
165168

169+
/**
170+
* Serialize a map of externally tagged union object arrays.
171+
* <p>
172+
* If {@link JsonpMapperFeatures#SERIALIZE_TYPED_KEYS} is <code>true</code> (the default), the typed keys encoding
173+
* (<code>type#name</code>) is used.
174+
*/
166175
static <T extends JsonpSerializable & TaggedUnion<? extends JsonEnum, ?>> void serializeTypedKeysArray(
167176
Map<String, List<T>> map, JsonGenerator generator, JsonpMapper mapper
168177
) {
169178
generator.writeStartObject();
170-
for (Map.Entry<String, List<T>> entry: map.entrySet()) {
171-
List<T> list = entry.getValue();
172-
if (list.isEmpty()) {
173-
continue; // We can't know the kind, skip this entry
174-
}
175179

176-
generator.writeKey(list.get(0)._kind().jsonValue() + "#" + entry.getKey());
177-
generator.writeStartArray();
178-
for (T value: list) {
179-
value.serialize(generator, mapper);
180+
if (mapper.attribute(JsonpMapperFeatures.SERIALIZE_TYPED_KEYS, true)) {
181+
for (Map.Entry<String, List<T>> entry: map.entrySet()) {
182+
List<T> list = entry.getValue();
183+
if (list.isEmpty()) {
184+
continue; // We can't know the kind, skip this entry
185+
}
186+
187+
generator.writeKey(list.get(0)._kind().jsonValue() + "#" + entry.getKey());
188+
generator.writeStartArray();
189+
for (T value: list) {
190+
value.serialize(generator, mapper);
191+
}
192+
generator.writeEnd();
193+
}
194+
} else {
195+
for (Map.Entry<String, List<T>> entry: map.entrySet()) {
196+
generator.writeKey(entry.getKey());
197+
generator.writeStartArray();
198+
for (T value: entry.getValue()) {
199+
value.serialize(generator, mapper);
200+
}
201+
generator.writeEnd();
180202
}
181-
generator.writeEnd();
182203
}
204+
183205
generator.writeEnd();
184206
}
185207

186208
/**
187-
* Serialize an externally tagged union using the typed keys encoding, without the enclosing start/end object.
209+
* Serialize a map of externally tagged union objects, without the enclosing start/end object.
210+
* <p>
211+
* If {@link JsonpMapperFeatures#SERIALIZE_TYPED_KEYS} is <code>true</code> (the default), the typed keys encoding
212+
* (<code>type#name</code>) is used.
188213
*/
189214
static <T extends JsonpSerializable & TaggedUnion<? extends JsonEnum, ?>> void serializeTypedKeysInner(
190215
Map<String, T> map, JsonGenerator generator, JsonpMapper mapper
191216
) {
192-
for (Map.Entry<String, T> entry: map.entrySet()) {
193-
T value = entry.getValue();
194-
generator.writeKey(value._kind().jsonValue() + "#" + entry.getKey());
195-
value.serialize(generator, mapper);
217+
if (mapper.attribute(JsonpMapperFeatures.SERIALIZE_TYPED_KEYS, true)) {
218+
for (Map.Entry<String, T> entry: map.entrySet()) {
219+
T value = entry.getValue();
220+
generator.writeKey(value._kind().jsonValue() + "#" + entry.getKey());
221+
value.serialize(generator, mapper);
222+
}
223+
} else {
224+
for (Map.Entry<String, T> entry: map.entrySet()) {
225+
generator.writeKey(entry.getKey());
226+
entry.getValue().serialize(generator, mapper);
227+
}
196228
}
197229
}
198230
}

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

+7-4
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ default <T> T attribute(String name, T defaultValue) {
7777
}
7878

7979
/**
80-
* Create a new mapper with a named attribute that delegates to this one.
80+
* Create a new mapper with an additional attribute.
81+
* <p>
82+
* The {@link JsonpMapperFeatures} class contains the names of attributes that all implementations of
83+
* <code>JsonpMapper</code> must implement.
84+
*
85+
* @see JsonpMapperFeatures
8186
*/
82-
default <T> JsonpMapper withAttribute(String name, T value) {
83-
return new AttributedJsonpMapper(this, name, value);
84-
}
87+
<T> JsonpMapper withAttribute(String name, T value);
8588
}

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

+29
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,41 @@
2525

2626
import javax.annotation.Nullable;
2727
import java.lang.reflect.Field;
28+
import java.util.Collections;
29+
import java.util.HashMap;
30+
import java.util.Map;
2831

2932
public abstract class JsonpMapperBase implements JsonpMapper {
3033

3134
/** Get a serializer when none of the builtin ones are applicable */
3235
protected abstract <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz);
3336

37+
private Map<String, Object> attributes;
38+
39+
@Nullable
40+
@Override
41+
@SuppressWarnings("unchecked")
42+
public <T> T attribute(String name) {
43+
return attributes == null ? null : (T)attributes.get(name);
44+
}
45+
46+
/**
47+
* Updates attributes to a copy of the current ones with an additional key/value pair.
48+
*
49+
* Mutates the current mapper, intended to be used in implementations of {@link #withAttribute(String, Object)}
50+
*/
51+
protected JsonpMapperBase addAttribute(String name, Object value) {
52+
if (attributes == null) {
53+
this.attributes = Collections.singletonMap(name, value);
54+
} else {
55+
Map<String, Object> newAttrs = new HashMap<>(attributes.size() + 1);
56+
newAttrs.putAll(attributes);
57+
newAttrs.put(name, value);
58+
this.attributes = newAttrs;
59+
}
60+
return this;
61+
}
62+
3463
@Override
3564
public <T> T deserialize(JsonParser parser, Class<T> clazz) {
3665
JsonpDeserializer<T> deserializer = findDeserializer(clazz);

java-client/src/main/java/co/elastic/clients/json/AttributedJsonpMapper.java renamed to java-client/src/main/java/co/elastic/clients/json/JsonpMapperFeatures.java

+5-21
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,11 @@
1919

2020
package co.elastic.clients.json;
2121

22-
import javax.annotation.Nullable;
23-
24-
class AttributedJsonpMapper extends DelegatingJsonpMapper {
25-
26-
private final String name;
27-
private final Object value;
22+
/**
23+
* Defines attribute names for {@link JsonpMapper} features.
24+
*/
25+
public class JsonpMapperFeatures {
2826

29-
AttributedJsonpMapper(JsonpMapper mapper, String name, Object value) {
30-
super(mapper);
31-
this.name = name;
32-
this.value = value;
33-
}
27+
public static final String SERIALIZE_TYPED_KEYS = JsonpMapperFeatures.class.getName() + ":SERIALIZE_TYPED_KEYS";
3428

35-
@Override
36-
@Nullable
37-
@SuppressWarnings("unchecked")
38-
public <T> T attribute(String name) {
39-
if (this.name.equals(name)) {
40-
return (T)this.value;
41-
} else {
42-
return mapper.attribute(name);
43-
}
44-
}
4529
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ public SimpleJsonpMapper() {
7777
this(true);
7878
}
7979

80+
@Override
81+
public <T> JsonpMapper withAttribute(String name, T value) {
82+
return new SimpleJsonpMapper(this.ignoreUnknownFields).addAttribute(name, value);
83+
}
84+
8085
@Override
8186
public boolean ignoreUnknownFields() {
8287
return ignoreUnknownFields;

java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java

+17-5
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,30 @@ public class JacksonJsonpMapper extends JsonpMapperBase {
3939
private final JacksonJsonProvider provider;
4040
private final ObjectMapper objectMapper;
4141

42+
private JacksonJsonpMapper(ObjectMapper objectMapper, JacksonJsonProvider provider) {
43+
this.objectMapper = objectMapper;
44+
this.provider = provider;
45+
}
46+
4247
public JacksonJsonpMapper(ObjectMapper objectMapper) {
43-
this.objectMapper = objectMapper
44-
.configure(SerializationFeature.INDENT_OUTPUT, false)
45-
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
46-
// Creating the json factory from the mapper ensures it will be returned by JsonParser.getCodec()
47-
this.provider = new JacksonJsonProvider(this.objectMapper.getFactory());
48+
this(
49+
objectMapper
50+
.configure(SerializationFeature.INDENT_OUTPUT, false)
51+
.setSerializationInclusion(JsonInclude.Include.NON_NULL),
52+
// Creating the json factory from the mapper ensures it will be returned by JsonParser.getCodec()
53+
new JacksonJsonProvider(objectMapper.getFactory())
54+
);
4855
}
4956

5057
public JacksonJsonpMapper() {
5158
this(new ObjectMapper());
5259
}
5360

61+
@Override
62+
public <T> JsonpMapper withAttribute(String name, T value) {
63+
return new JacksonJsonpMapper(this.objectMapper, this.provider).addAttribute(name, value);
64+
}
65+
5466
/**
5567
* Returns the underlying Jackson mapper.
5668
*/

java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public JsonbJsonpMapper() {
5454
this(JsonpUtils.provider(), JsonbProvider.provider());
5555
}
5656

57+
@Override
58+
public <T> JsonpMapper withAttribute(String name, T value) {
59+
return new JsonbJsonpMapper(this.jsonProvider, this.jsonb).addAttribute(name, value);
60+
}
61+
5762
@Override
5863
protected <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz) {
5964
return new Deserializer<>(clazz);

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

+24-13
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,33 @@ public B withJson(JsonParser parser, JsonpMapper mapper) {
4747
}
4848

4949
// Generic parameters are always deserialized to JsonData unless the parent mapper can provide a deserializer
50-
mapper = new DelegatingJsonpMapper(mapper) {
51-
@Override
52-
public <T> T attribute(String name) {
53-
T attr = mapper.attribute(name);
54-
if (attr == null && name.startsWith("co.elastic.clients:Deserializer")) {
55-
@SuppressWarnings("unchecked")
56-
T result = (T)JsonData._DESERIALIZER;
57-
return result;
58-
} else {
59-
return attr;
60-
}
61-
}
62-
};
50+
mapper = new WithJsonMapper(mapper);
6351

6452
@SuppressWarnings("unchecked")
6553
ObjectDeserializer<B> builderDeser = (ObjectDeserializer<B>) DelegatingDeserializer.unwrap(classDeser);
6654
return builderDeser.deserialize(self(), parser, mapper, parser.next());
6755
}
56+
57+
private static class WithJsonMapper extends DelegatingJsonpMapper {
58+
WithJsonMapper(JsonpMapper parent) {
59+
super(parent);
60+
}
61+
62+
@Override
63+
public <T> T attribute(String name) {
64+
T attr = mapper.attribute(name);
65+
if (attr == null && name.startsWith("co.elastic.clients:Deserializer")) {
66+
@SuppressWarnings("unchecked")
67+
T result = (T)JsonData._DESERIALIZER;
68+
return result;
69+
} else {
70+
return attr;
71+
}
72+
}
73+
74+
@Override
75+
public <T> JsonpMapper withAttribute(String name, T value) {
76+
return new WithJsonMapper(this.mapper.withAttribute(name, value));
77+
}
78+
}
6879
}

0 commit comments

Comments
 (0)