Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for disabling typed keys serialization #1296

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This section is for maintaining a changelog for all breaking changes for the cli
## [Unreleased 2.x]

### Added
- Added support for disabling typed keys serialization ([#1296](https://github.com/opensearch-project/opensearch-java/pull/1296))

### Dependencies

Expand Down
16 changes: 16 additions & 0 deletions guides/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ private String toJson(JsonpSerializable object) {
}
```

### Disabling Typed Keys Serialization
By default, the JSON serialization of the OpenSearch Java client uses typed keys for certain types, notably Aggregations.
This is done for the benefit of unambiguous deserialization, but may result in JSON output that is incompatible with use-cases expecting OpenSearch's default untyped keys.
You may disable this behavior by setting the `JsonpMapperAttributes.SERIALIZE_TYPED_KEYS` attribute to `false` on a JsonpMapper instance.
For example, the following code demonstrates how to serialize a SearchResponse without typed keys:
```java
private String withoutTypedKeys(OpenSearchClient client, SearchResponse response) {
JsonpMapper mapper = client._transport().jsonpMapper().withAttribute(JsonpMapperAttributes.SERIALIZE_TYPED_KEYS, false);
StringWriter writer = new StringWriter();
try (JsonGenerator generator = mapper.jsonProvider().createGenerator(writer)) {
response.serialize(generator, mapper);
}
return writer.toString();
}
```

## Deserialization

For demonstration let's consider an IndexTemplateMapping JSON String.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonParser;

@Deprecated
class AttributedJsonpMapper implements JsonpMapper {

private final JsonpMapper mapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,17 @@ public void deserializeEntry(String key, JsonParser parser, JsonpMapper mapper,
JsonGenerator generator,
JsonpMapper mapper
) {
for (Map.Entry<String, T> entry : map.entrySet()) {
T value = entry.getValue();
generator.writeKey(value._kind().jsonValue() + "#" + entry.getKey());
value.serialize(generator, mapper);
if (mapper.attribute(JsonpMapperAttributes.SERIALIZE_TYPED_KEYS, true)) {
for (Map.Entry<String, T> entry : map.entrySet()) {
T value = entry.getValue();
generator.writeKey(value._kind().jsonValue() + "#" + entry.getKey());
value.serialize(generator, mapper);
}
} else {
for (Map.Entry<String, T> entry : map.entrySet()) {
generator.writeKey(entry.getKey());
entry.getValue().serialize(generator, mapper);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,12 @@ default <T> T attribute(String name, T defaultValue) {

/**
* Create a new mapper with a named attribute that delegates to this one.
*
* @implNote The default implementation will be removed in a future release, and inheritors will be required to implement it themselves.
* See {@link org.opensearch.client.json.jsonb.JsonbJsonpMapper#withAttribute(String, Object)} and {@link org.opensearch.client.json.jackson.JacksonJsonpMapper#withAttribute(String, Object)} for examples.
*/
default <T> JsonpMapper withAttribute(String name, T value) {
// noinspection deprecation
return new AttributedJsonpMapper(this, name, value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.client.json;

public final class JsonpMapperAttributes {
private JsonpMapperAttributes() {}

public static final String SERIALIZE_TYPED_KEYS = JsonpMapperAttributes.class.getName() + ":SERIALIZE_TYPED_KEYS";
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,45 @@
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonParser;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;

public abstract class JsonpMapperBase implements JsonpMapper {
@Nullable
private Map<String, Object> attributes;

protected JsonpMapperBase() {}

protected JsonpMapperBase(JsonpMapperBase o) {
this.attributes = o.attributes; // We always copy in `setAttribute` so no need to copy here.
}

@Override
public <T> T attribute(String name) {
// noinspection unchecked
return attributes == null ? null : (T) attributes.get(name);
}

/**
* Updates attributes to a copy of the current ones with an additional key/value pair.
* Mutates the current mapper, intended to be used in implementations of {@link #withAttribute(String, Object)}
*/
protected JsonpMapperBase addAttribute(String name, Object value) {
if (attributes == null) {
this.attributes = Collections.singletonMap(name, value);
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to skip attributes recreation if the one in question is set already?

} else if (!this.attributes.containsKey(name) || !Objects.equals(this.attributes.get(name), value) {

// Copy the map to avoid modifying the original in case it was shared.
// We're generally only ever called from implementations' `withAttribute` methods which are intended
// to construct new instances rather than modify existing ones.
Map<String, Object> attributes = new HashMap<>(this.attributes.size() + 1);
attributes.putAll(this.attributes);
attributes.put(name, value);
this.attributes = attributes;
}
return this;
}

/** Get a serializer when none of the builtin ones are applicable */
protected abstract <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
import org.opensearch.client.json.JsonpSerializer;

public class JacksonJsonpMapper extends JsonpMapperBase {

private final JacksonJsonProvider provider;
private final ObjectMapper objectMapper;

Expand All @@ -68,6 +67,17 @@ public JacksonJsonpMapper() {
);
}

private JacksonJsonpMapper(JacksonJsonpMapper o) {
super(o);
this.provider = o.provider;
this.objectMapper = o.objectMapper;
}

@Override
public <T> JsonpMapper withAttribute(String name, T value) {
return new JacksonJsonpMapper(this).addAttribute(name, value);
}

/**
* Returns the underlying Jackson mapper.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ public JsonbJsonpMapper() {
this(JsonProvider.provider(), JsonbProvider.provider());
}

private JsonbJsonpMapper(JsonbJsonpMapper o) {
super(o);
this.jsonProvider = o.jsonProvider;
this.jsonb = o.jsonb;
}

@Override
public <T> JsonpMapper withAttribute(String name, T value) {
return new JsonbJsonpMapper(this).addAttribute(name, value);
}

@Override
protected <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz) {
return new Deserializer<>(clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,12 @@

package org.opensearch.client.opensearch.json.jackson;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.json.stream.JsonParser;
import jakarta.json.stream.JsonParser.Event;
import java.io.StringReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import org.junit.Test;
import org.opensearch.client.json.JsonpDeserializer;
import org.opensearch.client.json.JsonpMapper;
import org.opensearch.client.json.JsonpMapperBase;
import org.opensearch.client.json.jackson.JacksonJsonProvider;
import org.opensearch.client.json.jackson.JacksonJsonpMapper;
import org.opensearch.client.opensearch.core.MsearchResponse;
Expand Down Expand Up @@ -176,7 +170,7 @@ public void testMultiSearchResponse() {
+ " ]\n"
+ "}\n";

JsonpMapper mapper = new AttributedJacksonJsonpMapper().withAttribute(
JsonpMapper mapper = new JacksonJsonpMapper().withAttribute(
"org.opensearch.client:Deserializer:_global.msearch.TDocument",
JsonpDeserializer.of(Foo.class)
);
Expand All @@ -189,46 +183,6 @@ public void testMultiSearchResponse() {
assertEquals((Integer) 200, response.responses().get(1).result().status());
}

public static class AttributedJacksonJsonpMapper extends JacksonJsonpMapper {
private Map<String, Object> attributes;

public AttributedJacksonJsonpMapper() {
super();
}

public AttributedJacksonJsonpMapper(ObjectMapper objectMapper) {
super(objectMapper);
}

@Nullable
@Override
@SuppressWarnings("unchecked")
public <T> T attribute(String name) {
return attributes == null ? null : (T) attributes.get(name);
}

/**
* Updates attributes to a copy of the current ones with an additional key/value pair.
* Mutates the current mapper, intended to be used in implementations of {@link #withAttribute(String, Object)}
*/
protected JsonpMapperBase addAttribute(String name, Object value) {
if (attributes == null) {
this.attributes = Collections.singletonMap(name, value);
} else {
Map<String, Object> newAttrs = new HashMap<>(attributes.size() + 1);
newAttrs.putAll(attributes);
newAttrs.put(name, value);
this.attributes = newAttrs;
}
return this;
}

@Override
public <T> JsonpMapper withAttribute(String name, T value) {
return new AttributedJacksonJsonpMapper(objectMapper()).addAttribute(name, value);
}
}

public static class Foo {
private int x;
private boolean y;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@

package org.opensearch.client.opensearch.model;

import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.ArrayList;
import java.util.Collections;
import org.junit.Test;
import org.opensearch.client.json.JsonpDeserializer;
import org.opensearch.client.json.JsonpMapperAttributes;
import org.opensearch.client.opensearch._types.aggregations.Aggregate;
import org.opensearch.client.opensearch._types.aggregations.AvgAggregate;
import org.opensearch.client.opensearch._types.aggregations.StringTermsAggregate;
Expand Down Expand Up @@ -113,4 +116,17 @@ public void testAdditionalProperties() {
assertEquals("key_2", foo.buckets().array().get(1).key());
assertEquals(2.0, foo.buckets().array().get(1).aggregations().get("bar").avg().value(), 0.01);
}

@Test
public void testDisablingSerializeTypedKeys() {
SearchResponse<ObjectNode> resp = new SearchResponse.Builder<ObjectNode>().aggregations(
"aggKey",
v -> v.lterms(lv -> lv.buckets(b -> b.array(new ArrayList<>())).sumOtherDocCount(0))
).took(0).timedOut(false).shards(s -> s.failed(0).successful(1).total(1)).hits(h -> h.hits(new ArrayList<>())).build();

String json =
"{\"took\":0,\"timed_out\":false,\"_shards\":{\"failed\":0,\"successful\":1,\"total\":1},\"hits\":{\"hits\":[]},\"aggregations\":{\"aggKey\":{\"buckets\":[],\"sum_other_doc_count\":0}}}";

assertEquals(json, toJson(resp, mapper.withAttribute(JsonpMapperAttributes.SERIALIZE_TYPED_KEYS, false)));
}
}
Loading