Skip to content

Commit 61dba33

Browse files
github-actions[bot]swallez
andauthoredJun 22, 2022
Allow serializing aggregations without typed keys (#316) (#326)
Co-authored-by: Sylvain Wallez <[email protected]>
·
v8.3.3v8.3.0
1 parent 771a4ca commit 61dba33

File tree

12 files changed

+436
-61
lines changed

12 files changed

+436
-61
lines changed
 

‎docs/troubleshooting/index.asciidoc‎

Lines changed: 2 additions & 0 deletions
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[]
Lines changed: 24 additions & 0 deletions
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‎

Lines changed: 48 additions & 16 deletions
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‎

Lines changed: 7 additions & 4 deletions
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‎

Lines changed: 29 additions & 0 deletions
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‎

Lines changed: 5 additions & 21 deletions
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‎

Lines changed: 5 additions & 0 deletions
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‎

Lines changed: 17 additions & 5 deletions
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‎

Lines changed: 5 additions & 0 deletions
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‎

Lines changed: 24 additions & 13 deletions
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
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.documentation.troubleshooting;
21+
22+
import co.elastic.clients.documentation.DocTestsTransport;
23+
import co.elastic.clients.elasticsearch.ElasticsearchClient;
24+
import co.elastic.clients.elasticsearch.core.SearchResponse;
25+
import co.elastic.clients.elasticsearch.core.search.TotalHitsRelation;
26+
import co.elastic.clients.json.JsonpMapper;
27+
import co.elastic.clients.json.JsonpMapperFeatures;
28+
import jakarta.json.stream.JsonGenerator;
29+
import org.junit.jupiter.api.Assertions;
30+
import org.junit.jupiter.api.Test;
31+
32+
import java.io.StringWriter;
33+
import java.util.Collections;
34+
35+
public class TroubleShootingTests extends Assertions {
36+
37+
@Test
38+
public void testMapProperty() {
39+
40+
SearchResponse<Void> searchResponse = new SearchResponse.Builder<Void>()
41+
.aggregations(
42+
"price", _2 -> _2
43+
.avg(_3 -> _3.value(3.14))
44+
)
45+
// Required properties on a SearchResponse
46+
.took(1)
47+
.shards(_1 -> _1.successful(1).failed(0).total(1))
48+
.hits(_1 -> _1
49+
.total(_2 -> _2.value(0).relation(TotalHitsRelation.Eq))
50+
.hits(Collections.emptyList())
51+
)
52+
.timedOut(false)
53+
.build();
54+
55+
String json = "{\"took\":1,\"timed_out\":false,\"_shards\":{\"failed\":0.0,\"successful\":1.0,\"total\":1.0}," +
56+
"\"hits\":{\"total\":{\"relation\":\"eq\",\"value\":0},\"hits\":[]},\"aggregations\":{\"avg#price\":{\"value\":3.14}}}";
57+
58+
DocTestsTransport transport = new DocTestsTransport();
59+
ElasticsearchClient esClient = new ElasticsearchClient(transport);
60+
61+
{
62+
//tag::aggregation-typed-keys
63+
JsonpMapper mapper = esClient._jsonpMapper();
64+
65+
StringWriter writer = new StringWriter();
66+
try (JsonGenerator generator = mapper.jsonProvider().createGenerator(writer)) {
67+
mapper.serialize(searchResponse, generator);
68+
}
69+
String result = writer.toString();
70+
71+
// The aggregation property provides the "avg" type and "price" name
72+
assertTrue(result.contains("\"aggregations\":{\"avg#price\":{\"value\":3.14}}}"));
73+
//end::aggregation-typed-keys
74+
}
75+
76+
{
77+
//tag::aggregation-no-typed-keys
78+
// Create a new mapper with the typed_keys feature disabled
79+
JsonpMapper mapper = esClient._jsonpMapper()
80+
.withAttribute(JsonpMapperFeatures.SERIALIZE_TYPED_KEYS, false);
81+
82+
StringWriter writer = new StringWriter();
83+
try (JsonGenerator generator = mapper.jsonProvider().createGenerator(writer)) {
84+
mapper.serialize(searchResponse, generator);
85+
}
86+
String result = writer.toString();
87+
88+
// The aggregation only provides the "price" name
89+
assertTrue(result.contains("\"aggregations\":{\"price\":{\"value\":3.14}}}"));
90+
//end::aggregation-no-typed-keys
91+
}
92+
}
93+
}

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

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@
2626
import co.elastic.clients.elasticsearch.core.SearchResponse;
2727
import co.elastic.clients.elasticsearch.core.search.TotalHitsRelation;
2828
import co.elastic.clients.json.JsonpDeserializer;
29+
import co.elastic.clients.json.JsonpMapper;
30+
import co.elastic.clients.json.JsonpMapperFeatures;
2931
import co.elastic.clients.util.ListBuilder;
3032
import co.elastic.clients.util.MapBuilder;
33+
import jakarta.json.spi.JsonProvider;
34+
import jakarta.json.stream.JsonGenerator;
3135
import org.junit.jupiter.api.Test;
3236

37+
import java.io.StringWriter;
3338
import java.util.Collections;
3439

3540
public class TypedKeysTest extends ModelTestCase {
@@ -64,6 +69,34 @@ public void testMapProperty() {
6469

6570
}
6671

72+
@Test
73+
public void testMapPropertyWithoutTypedKeys() {
74+
75+
SearchResponse<Void> resp = new SearchResponse.Builder<Void>()
76+
.aggregations(
77+
"foo", _2 -> _2
78+
.avg(_3 -> _3.value(3.14))
79+
)
80+
// Required properties on a SearchResponse
81+
.took(1)
82+
.shards(_1 -> _1.successful(1).failed(0).total(1))
83+
.hits(_1 -> _1
84+
.total(_2 -> _2.value(0).relation(TotalHitsRelation.Eq))
85+
.hits(Collections.emptyList())
86+
)
87+
.timedOut(false)
88+
.build();
89+
90+
// Note "foo" and not "avg#foo" below
91+
String json = "{\"took\":1,\"timed_out\":false,\"_shards\":{\"failed\":0.0,\"successful\":1.0,\"total\":1.0}," +
92+
"\"hits\":{\"total\":{\"relation\":\"eq\",\"value\":0},\"hits\":[]},\"aggregations\":{\"foo\":{\"value\":3.14}}}";
93+
94+
JsonpMapper newMapper = mapper.withAttribute(JsonpMapperFeatures.SERIALIZE_TYPED_KEYS, false);
95+
96+
assertEquals(json, toJson(resp, newMapper));
97+
}
98+
99+
67100
@Test
68101
public void testAdditionalProperties() {
69102

@@ -105,8 +138,8 @@ public void testAdditionalProperties() {
105138
String json = "{\"took\":1,\"timed_out\":false,\"_shards\":{\"failed\":0.0,\"successful\":1.0,\"total\":1.0}," +
106139
"\"hits\":{\"total\":{\"relation\":\"eq\",\"value\":0},\"hits\":[]}," +
107140
"\"aggregations\":{\"sterms#foo\":{\"buckets\":[" +
108-
"{\"avg#bar\":{\"value\":1.0},\"doc_count\":1,\"key\":\"key_1\"}," +
109-
"{\"avg#bar\":{\"value\":2.0},\"doc_count\":2,\"key\":\"key_2\"}" +
141+
"{\"avg#bar\":{\"value\":1.0},\"doc_count\":1,\"key\":\"key_1\"}," +
142+
"{\"avg#bar\":{\"value\":2.0},\"doc_count\":2,\"key\":\"key_2\"}" +
110143
"],\"sum_other_doc_count\":1}}}";
111144

112145
assertEquals(json, toJson(resp));
@@ -120,4 +153,146 @@ public void testAdditionalProperties() {
120153
assertEquals("key_2", foo.buckets().array().get(1).key());
121154
assertEquals(2.0, foo.buckets().array().get(1).aggregations().get("bar").avg().value(), 0.01);
122155
}
156+
157+
// Example taken from
158+
// https://www.elastic.co/guide/en/elasticsearch/reference/8.2/search-aggregations-bucket-reverse-nested-aggregation.html
159+
private static final String nestedJsonWithTypedKeys = "{\n" +
160+
" \"took\": 0," +
161+
" \"timed_out\": false," +
162+
" \"_shards\": {\n" +
163+
" \"successful\": 1,\n" +
164+
" \"failed\": 0,\n" +
165+
" \"skipped\": 0,\n" +
166+
" \"total\": 1\n" +
167+
" },\n" +
168+
" \"hits\": {\n" +
169+
" \"hits\": [],\n" +
170+
" \"total\": {\n" +
171+
" \"relation\": \"eq\",\n" +
172+
" \"value\": 5\n" +
173+
" },\n" +
174+
" \"max_score\": null\n" +
175+
" }," +
176+
" \"aggregations\" : {\n" +
177+
" \"nested#comments\" : {\n" +
178+
" \"doc_count\" : 3,\n" +
179+
" \"sterms#top_usernames\" : {\n" +
180+
" \"doc_count_error_upper_bound\" : 0,\n" +
181+
" \"sum_other_doc_count\" : 0,\n" +
182+
" \"buckets\" : [\n" +
183+
" {\n" +
184+
" \"key\" : \"dan\",\n" +
185+
" \"doc_count\" : 3,\n" +
186+
" \"reverse_nested#comment_to_issue\" : {\n" +
187+
" \"doc_count\" : 1,\n" +
188+
" \"sterms#top_tags_per_comment\" : {\n" +
189+
" \"doc_count_error_upper_bound\" : 0,\n" +
190+
" \"sum_other_doc_count\" : 0,\n" +
191+
" \"buckets\" : [\n" +
192+
" {\n" +
193+
" \"key\" : \"tag1\",\n" +
194+
" \"doc_count\" : 1\n" +
195+
" },\n" +
196+
" {\n" +
197+
" \"key\" : \"tag2\",\n" +
198+
" \"doc_count\" : 1\n" +
199+
" }\n" +
200+
" ]\n" +
201+
" }\n" +
202+
" }\n" +
203+
" }\n" +
204+
" ]\n" +
205+
" }\n" +
206+
" }\n" +
207+
" }\n" +
208+
"}";
209+
210+
@Test
211+
public void testSerializeNested() {
212+
213+
SearchResponse<?> response = fromJson(nestedJsonWithTypedKeys, SearchResponse.class);
214+
215+
// Check some deeply nested properties
216+
StringTermsBucket bucket = response
217+
.aggregations().get("comments").nested()
218+
.aggregations().get("top_usernames").sterms()
219+
.buckets().array().get(0)
220+
.aggregations().get("comment_to_issue").reverseNested()
221+
.aggregations().get("top_tags_per_comment").sterms()
222+
.buckets().array().get(0);
223+
224+
assertEquals("tag1", bucket.key());
225+
assertEquals(1, bucket.docCount());
226+
227+
// Check that it's typed_keys encoded
228+
String serialized = toJson(response);
229+
assertTrue(serialized.contains("nested#comments"));
230+
assertTrue(serialized.contains("sterms#top_usernames"));
231+
assertTrue(serialized.contains("reverse_nested#comment_to_issue"));
232+
assertTrue(serialized.contains("sterms#top_tags_per_comment"));
233+
234+
{
235+
// Test direct serialization
236+
JsonProvider jsonProvider = mapper.jsonProvider();
237+
StringWriter stringWriter = new StringWriter();
238+
JsonGenerator generator = jsonProvider.createGenerator(stringWriter);
239+
response.serialize(generator, mapper);
240+
generator.close();
241+
242+
String directSerialized = stringWriter.toString();
243+
assertTrue(directSerialized.contains("nested#comments"));
244+
assertTrue(directSerialized.contains("sterms#top_usernames"));
245+
assertTrue(directSerialized.contains("reverse_nested#comment_to_issue"));
246+
assertTrue(directSerialized.contains("sterms#top_tags_per_comment"));
247+
248+
}
249+
250+
// Re-parse and re-check
251+
response = fromJson(serialized, SearchResponse.class);
252+
253+
bucket = response
254+
.aggregations().get("comments").nested()
255+
.aggregations().get("top_usernames").sterms()
256+
.buckets().array().get(0)
257+
.aggregations().get("comment_to_issue").reverseNested()
258+
.aggregations().get("top_tags_per_comment").sterms()
259+
.buckets().array().get(0);
260+
261+
assertEquals("tag1", bucket.key());
262+
assertEquals(1, bucket.docCount());
263+
264+
265+
JsonProvider jsonProvider = mapper.jsonProvider();
266+
StringWriter stringWriter = new StringWriter();
267+
JsonGenerator generator = jsonProvider.createGenerator(stringWriter);
268+
response.serialize(generator, mapper);
269+
generator.close();
270+
271+
System.out.println(stringWriter.toString());
272+
}
273+
274+
@Test
275+
public void testSerializeNestedWithoutTypedKeys() {
276+
277+
SearchResponse<?> response = fromJson(nestedJsonWithTypedKeys, SearchResponse.class);
278+
279+
// Check that it's typed_keys encoded
280+
String serialized = toJson(response);
281+
assertTrue(serialized.contains("nested#comments"));
282+
assertTrue(serialized.contains("sterms#top_usernames"));
283+
assertTrue(serialized.contains("reverse_nested#comment_to_issue"));
284+
assertTrue(serialized.contains("sterms#top_tags_per_comment"));
285+
286+
// Build the non-typed_keys version (replace 'type#' with 'type#name')
287+
serialized = serialized.replaceAll("\"[^\"]*#", "\"");
288+
assertFalse(serialized.contains("nested#comments"));
289+
assertFalse(serialized.contains("sterms#top_usernames"));
290+
assertFalse(serialized.contains("reverse_nested#comment_to_issue"));
291+
assertFalse(serialized.contains("sterms#top_tags_per_comment"));
292+
293+
// Serialize without typed keys
294+
JsonpMapper newMapper = mapper.withAttribute(JsonpMapperFeatures.SERIALIZE_TYPED_KEYS, false);
295+
assertEquals(serialized, toJson(response, newMapper));
296+
297+
}
123298
}

0 commit comments

Comments
 (0)
Please sign in to comment.