Global error filter in GraphQlTester.Builder

See gh-66
This commit is contained in:
Rossen Stoyanchev
2021-06-28 07:12:19 +01:00
parent 5cd1e266b4
commit 0b71596aa5
7 changed files with 200 additions and 92 deletions

View File

@@ -59,8 +59,8 @@ class DefaultGraphQlTester implements GraphQlTester {
private final RequestStrategy requestStrategy;
DefaultGraphQlTester(GraphQlService service, Configuration config, Duration responseTimeout) {
this(new GraphQlServiceRequestStrategy(service, config, responseTimeout));
DefaultGraphQlTester(GraphQlService service, GraphQlTesterBuilderConfig builderConfig) {
this(new GraphQlServiceRequestStrategy(service, builderConfig));
}
DefaultGraphQlTester(RequestStrategy requestStrategy) {
@@ -86,29 +86,34 @@ class DefaultGraphQlTester implements GraphQlTester {
private final GraphQlService service;
private final BuilderDelegate delegate = new BuilderDelegate();
private final GraphQlTesterBuilderConfig builderConfig = new GraphQlTesterBuilderConfig();
DefaultBuilder(GraphQlService service) {
Assert.notNull(service, "GraphQlService is required.");
this.service = service;
}
@Override
public DefaultBuilder errorFilter(Predicate<GraphQLError> predicate) {
this.builderConfig.errorFilter(predicate);
return this;
}
@Override
public DefaultBuilder jsonPathConfig(Configuration config) {
this.delegate.jsonPathConfig(config);
this.builderConfig.jsonPathConfig(config);
return this;
}
@Override
public DefaultBuilder responseTimeout(Duration timeout) {
this.delegate.responseTimeout(timeout);
this.builderConfig.responseTimeout(timeout);
return this;
}
@Override
public GraphQlTester build() {
return new DefaultGraphQlTester(
this.service, this.delegate.initJsonPathConfig(), this.delegate.getResponseTimeout());
return new DefaultGraphQlTester(this.service, this.builderConfig);
}
}
@@ -141,24 +146,30 @@ class DefaultGraphQlTester implements GraphQlTester {
*/
protected abstract static class AbstractDirectRequestStrategy implements RequestStrategy {
private final Configuration jsonPathConfig;
private final GraphQlTesterBuilderConfig builderConfig;
private final Duration responseTimeout;
protected AbstractDirectRequestStrategy(Configuration jsonPathConfig, Duration responseTimeout) {
this.jsonPathConfig = jsonPathConfig;
this.responseTimeout = responseTimeout;
protected AbstractDirectRequestStrategy(GraphQlTesterBuilderConfig builderConfig) {
this.builderConfig = builderConfig;
}
protected Duration getResponseTimeout() {
return this.responseTimeout;
@Nullable
private Predicate<GraphQLError> errorFilter() {
return this.builderConfig.getErrorFilter();
}
private Configuration jsonPathConfig() {
return this.builderConfig.getJsonPathConfig();
}
protected Duration responseTimeout() {
return this.builderConfig.getResponseTimeout();
}
@Override
public ResponseSpec execute(RequestInput input) {
ExecutionResult executionResult = executeInternal(input);
DocumentContext context = JsonPath.parse(executionResult.toSpecification(), this.jsonPathConfig);
return new DefaultResponseSpec(context, assertDecorator(input));
DocumentContext context = JsonPath.parse(executionResult.toSpecification(), jsonPathConfig());
return new DefaultResponseSpec(context, errorFilter(), assertDecorator(input));
}
@Override
@@ -171,7 +182,7 @@ class DefaultGraphQlTester implements GraphQlTester {
assertDecorator.accept(() -> AssertionErrors.assertTrue(
"Response has " + errors.size() + " unexpected error(s).", CollectionUtils.isEmpty(errors)));
return new DefaultSubscriptionSpec(result.getData(), this.jsonPathConfig, assertDecorator);
return new DefaultSubscriptionSpec(result.getData(), errorFilter(), jsonPathConfig(), assertDecorator);
}
/**
@@ -199,17 +210,15 @@ class DefaultGraphQlTester implements GraphQlTester {
private final GraphQlService graphQlService;
protected GraphQlServiceRequestStrategy(
GraphQlService service, Configuration jsonPathConfig, Duration responseTimeout) {
super(jsonPathConfig, responseTimeout);
protected GraphQlServiceRequestStrategy(GraphQlService service, GraphQlTesterBuilderConfig builderConfig) {
super(builderConfig);
Assert.notNull(service, "GraphQlService is required.");
this.graphQlService = service;
}
protected ExecutionResult executeInternal(RequestInput input) {
ExecutionInput executionInput = input.toExecutionInput();
ExecutionResult result = this.graphQlService.execute(executionInput).block(getResponseTimeout());
ExecutionResult result = this.graphQlService.execute(executionInput).block(responseTimeout());
Assert.notNull(result, "Expected ExecutionResult");
return result;
}
@@ -324,19 +333,28 @@ class DefaultGraphQlTester implements GraphQlTester {
private final Consumer<Runnable> assertDecorator;
ErrorsContainer(List<TestGraphQlError> errors, Consumer<Runnable> assertDecorator) {
ErrorsContainer(
List<TestGraphQlError> errors, @Nullable Predicate<GraphQLError> errorFilter,
Consumer<Runnable> assertDecorator) {
Assert.notNull(errors, "`errors` is required");
Assert.notNull(assertDecorator, "`assertDecorator` is required");
this.errors = errors;
this.assertDecorator = assertDecorator;
filterErrors(errorFilter);
}
void doAssert(Runnable task) {
this.assertDecorator.accept(task);
}
void filterErrors(Predicate<GraphQLError> errorPredicate) {
this.errors.forEach((error) -> error.filter(errorPredicate));
void filterErrors(@Nullable Predicate<GraphQLError> predicate) {
if (predicate != null) {
this.errors.forEach((error) -> {
// Error marked "filtered" if true
error.applyErrorFilterPredicate(predicate);
});
}
}
void consumeErrors(Consumer<List<GraphQLError>> consumer) {
@@ -345,8 +363,8 @@ class DefaultGraphQlTester implements GraphQlTester {
}
void verifyErrors() {
List<TestGraphQlError> unexpected = this.errors.stream().filter((error) -> !error.isExpected())
List<TestGraphQlError> unexpected = this.errors.stream()
.filter(error -> !error.isExpected())
.collect(Collectors.toList());
this.assertDecorator
@@ -366,14 +384,19 @@ class DefaultGraphQlTester implements GraphQlTester {
*/
private static class ResponseContainer extends ErrorsContainer {
private static final TypeRef<List<TestGraphQlError>> ERROR_LIST_TYPE = new TypeRef<List<TestGraphQlError>>() {};
private static final JsonPath ERRORS_PATH = JsonPath.compile("$.errors");
private final DocumentContext documentContext;
private final String jsonContent;
ResponseContainer(DocumentContext documentContext, Consumer<Runnable> assertDecorator) {
super(readErrors(documentContext), assertDecorator);
ResponseContainer(
DocumentContext documentContext, @Nullable Predicate<GraphQLError> errorFilter,
Consumer<Runnable> assertDecorator) {
super(readErrors(documentContext), errorFilter, assertDecorator);
this.documentContext = documentContext;
this.jsonContent = this.documentContext.jsonString();
}
@@ -381,8 +404,7 @@ class DefaultGraphQlTester implements GraphQlTester {
private static List<TestGraphQlError> readErrors(DocumentContext documentContext) {
Assert.notNull(documentContext, "DocumentContext is required");
try {
return documentContext.read(ERRORS_PATH, new TypeRef<List<TestGraphQlError>>() {
});
return documentContext.read(ERRORS_PATH, ERROR_LIST_TYPE);
}
catch (PathNotFoundException ex) {
return Collections.emptyList();
@@ -419,11 +441,14 @@ class DefaultGraphQlTester implements GraphQlTester {
/**
* Class constructor.
* @param documentContext the parsed response content
* @param errorFilter a globally defined filter for expected errors (to be ignored)
* @param assertDecorator decorator to apply around assertions, e.g. to add extra
* contextual information such as HTTP request and response body details
*/
protected DefaultResponseSpec(DocumentContext documentContext, Consumer<Runnable> assertDecorator) {
this.responseContainer = new ResponseContainer(documentContext, assertDecorator);
protected DefaultResponseSpec(
DocumentContext documentContext, @Nullable Predicate<GraphQLError> errorFilter,
Consumer<Runnable> assertDecorator) {
this.responseContainer = new ResponseContainer(documentContext, errorFilter, assertDecorator);
}
@Override
@@ -755,14 +780,19 @@ class DefaultGraphQlTester implements GraphQlTester {
private final Publisher<ExecutionResult> publisher;
@Nullable
private final Predicate<GraphQLError> errorFilter;
private final Configuration jsonPathConfig;
private final Consumer<Runnable> assertDecorator;
protected <T> DefaultSubscriptionSpec(Publisher<ExecutionResult> publisher, Configuration jsonPathConfig,
Consumer<Runnable> decorator) {
protected <T> DefaultSubscriptionSpec(
Publisher<ExecutionResult> publisher, @Nullable Predicate<GraphQLError> errorFilter,
Configuration jsonPathConfig, Consumer<Runnable> decorator) {
this.publisher = publisher;
this.errorFilter = errorFilter;
this.jsonPathConfig = jsonPathConfig;
this.assertDecorator = decorator;
}
@@ -771,7 +801,7 @@ class DefaultGraphQlTester implements GraphQlTester {
public Flux<ResponseSpec> toFlux() {
return Flux.from(this.publisher).map((result) -> {
DocumentContext context = JsonPath.parse(result.toSpecification(), this.jsonPathConfig);
return new DefaultResponseSpec(context, this.assertDecorator);
return new DefaultResponseSpec(context, this.errorFilter, this.assertDecorator);
});
}

View File

@@ -20,12 +20,14 @@ import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import org.springframework.graphql.RequestInput;
import org.springframework.graphql.web.WebGraphQlHandler;
@@ -69,34 +71,38 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
private final Supplier<RequestStrategy> requestStrategySupplier;
private final BuilderDelegate delegate = new BuilderDelegate();
private final GraphQlTesterBuilderConfig builderConfig = new GraphQlTesterBuilderConfig();
@Nullable
private HttpHeaders headers;
DefaultBuilder(WebTestClient client) {
this.requestStrategySupplier = () ->
new WebTestClientRequestStrategy(
client.mutate().responseTimeout(this.delegate.getResponseTimeout()).build(),
this.delegate.initJsonPathConfig());
this.requestStrategySupplier = () -> {
Duration timeout = this.builderConfig.getResponseTimeout();
WebTestClient clientToUse = client.mutate().responseTimeout(timeout).build();
return new WebTestClientRequestStrategy(clientToUse, this.builderConfig);
};
}
DefaultBuilder(WebGraphQlHandler handler) {
this.requestStrategySupplier = () ->
new WebGraphQlHandlerRequestStrategy(handler,
this.delegate.initJsonPathConfig(),
this.delegate.getResponseTimeout());
this.requestStrategySupplier = () -> new WebGraphQlHandlerRequestStrategy(handler, this.builderConfig);
}
@Override
public WebGraphQlTester.Builder errorFilter(Predicate<GraphQLError> predicate) {
this.builderConfig.errorFilter(predicate);
return this;
}
@Override
public DefaultBuilder jsonPathConfig(Configuration config) {
this.delegate.jsonPathConfig(config);
this.builderConfig.jsonPathConfig(config);
return this;
}
@Override
public DefaultBuilder responseTimeout(Duration timeout) {
this.delegate.responseTimeout(timeout);
this.builderConfig.responseTimeout(timeout);
return this;
}
@@ -131,11 +137,20 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
private final WebTestClient client;
private final Configuration jsonPathConfig;
private final GraphQlTesterBuilderConfig builderConfig;
WebTestClientRequestStrategy(WebTestClient client, Configuration jsonPathConfig) {
WebTestClientRequestStrategy(WebTestClient client, GraphQlTesterBuilderConfig builderConfig) {
this.client = client;
this.jsonPathConfig = jsonPathConfig;
this.builderConfig = builderConfig;
}
@Nullable
private Predicate<GraphQLError> errorFilter() {
return this.builderConfig.getErrorFilter();
}
private Configuration jsonPathConfig() {
return this.builderConfig.getJsonPathConfig();
}
@Override
@@ -155,9 +170,9 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
byte[] bytes = result.getResponseBodyContent();
Assert.notNull(bytes, "Expected GraphQL response content");
String content = new String(bytes, StandardCharsets.UTF_8);
DocumentContext documentContext = JsonPath.parse(content, this.jsonPathConfig);
DocumentContext documentContext = JsonPath.parse(content, jsonPathConfig());
return new DefaultResponseSpec(documentContext, result::assertWithDiagnostics);
return new DefaultResponseSpec(documentContext, errorFilter(), result::assertWithDiagnostics);
}
@Override
@@ -174,8 +189,9 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
.contentType(MediaType.TEXT_EVENT_STREAM)
.returnResult(TestExecutionResult.class);
return new DefaultSubscriptionSpec(exchangeResult.getResponseBody().cast(ExecutionResult.class),
this.jsonPathConfig, exchangeResult::assertWithDiagnostics);
return new DefaultSubscriptionSpec(
exchangeResult.getResponseBody().cast(ExecutionResult.class),
errorFilter(), jsonPathConfig(), exchangeResult::assertWithDiagnostics);
}
private HttpHeaders getHeaders(RequestInput requestInput) {
@@ -193,15 +209,15 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
private final WebGraphQlHandler graphQlHandler;
WebGraphQlHandlerRequestStrategy(WebGraphQlHandler handler, Configuration config, Duration responseTimeout) {
super(config, responseTimeout);
WebGraphQlHandlerRequestStrategy(WebGraphQlHandler handler, GraphQlTesterBuilderConfig builderConfig) {
super(builderConfig);
this.graphQlHandler = handler;
}
protected ExecutionResult executeInternal(RequestInput input) {
Assert.isInstanceOf(WebInput.class, input);
WebInput webInput = (WebInput) input;
ExecutionResult result = this.graphQlHandler.handle(webInput).block(getResponseTimeout());
ExecutionResult result = this.graphQlHandler.handle(webInput).block(responseTimeout());
Assert.notNull(result, "Expected ExecutionResult");
return result;
}

View File

@@ -78,6 +78,16 @@ public interface GraphQlTester {
*/
interface Builder<T extends Builder<T>> {
/**
* Add a global filter for expected errors. All errors that match the
* given predicate are treated as expected and ignored on
* {@link GraphQlTester.ErrorSpec#verify()} or when
* {@link TraverseSpec#path(String) traversing} to a data path.
* @param predicate the error filter to add
* @return the same builder instance
*/
T errorFilter(Predicate<GraphQLError> predicate);
/**
* Provide JSONPath configuration settings, including a
* {@link com.jayway.jsonpath.spi.json.JsonProvider} as well as a
@@ -424,7 +434,7 @@ public interface GraphQlTester {
* Add a filter for expected errors. All errors that match the predicate are
* treated as expected and ignored on {@link #verify()} or when
* {@link TraverseSpec#path(String) traversing} to a data path.
* @param errorPredicate the predicate to add
* @param errorPredicate the error filter to add
* @return the same spec to add more filters before {@link #verify()}
*/
ErrorSpec filter(Predicate<GraphQLError> errorPredicate);

View File

@@ -16,22 +16,25 @@
package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import graphql.GraphQLError;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Assist with collecting the input for {@link GraphQlTester.Builder},
* essentially to avoid challenges with generics in the builder hierarchy.
* Holds the input required for {@link GraphQlTester.Builder}, providing a
* convenient way to pass it together, while also helping to avoid challenges
* with builder hierarchy generics.
*
* @author Rossen Stoyanchev
*/
final class BuilderDelegate {
final class GraphQlTesterBuilderConfig {
private static final boolean jackson2Present;
@@ -42,11 +45,18 @@ final class BuilderDelegate {
}
@Nullable
private Predicate<GraphQLError> errorFilter;
@Nullable
private Configuration jsonPathConfig;
private Duration responseTimeout = Duration.ofSeconds(5);
public void errorFilter(Predicate<GraphQLError> predicate) {
this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate);
}
public void jsonPathConfig(@Nullable Configuration config) {
this.jsonPathConfig = config;
}
@@ -56,16 +66,17 @@ final class BuilderDelegate {
this.responseTimeout = timeout;
}
public Configuration initJsonPathConfig() {
if (this.jsonPathConfig != null) {
return this.jsonPathConfig;
}
else if (jackson2Present) {
return Jackson2Configuration.create();
}
else {
return Configuration.builder().build();
@Nullable
public Predicate<GraphQLError> getErrorFilter() {
return this.errorFilter;
}
public Configuration getJsonPathConfig() {
if (this.jsonPathConfig == null) {
this.jsonPathConfig = (jackson2Present ?
Jackson2Configuration.create() : Configuration.builder().build());
}
return this.jsonPathConfig;
}
public Duration getResponseTimeout() {

View File

@@ -121,7 +121,7 @@ class TestGraphQlError implements GraphQLError {
* Mark this error as expected if it matches the predicate.
* @param predicate the error predicate
*/
void filter(Predicate<GraphQLError> predicate) {
void applyErrorFilterPredicate(Predicate<GraphQLError> predicate) {
this.expected |= predicate.test(this);
}

View File

@@ -60,10 +60,10 @@ public class GraphQlTesterTests {
private final GraphQlService service = mock(GraphQlService.class);
private final ArgumentCaptor<ExecutionInput> inputCaptor = ArgumentCaptor.forClass(ExecutionInput.class);
private final GraphQlTester graphQlTester = GraphQlTester.create(this.service);
private final ArgumentCaptor<ExecutionInput> inputCaptor = ArgumentCaptor.forClass(ExecutionInput.class);
@Test
void pathAndValueExistsAndEmptyChecks() throws Exception {
@@ -77,7 +77,7 @@ public class GraphQlTesterTests {
spec.path("me.friends").valueIsEmpty();
spec.path("hero").pathDoesNotExist().valueDoesNotExist().valueIsEmpty();
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -97,7 +97,7 @@ public class GraphQlTesterTests {
.as("Extended fields should fail in strict mode")
.hasMessageContaining("Unexpected: name");
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -127,7 +127,7 @@ public class GraphQlTesterTests {
.entity(new ParameterizedTypeReference<Map<String, MovieCharacter>>() {})
.isEqualTo(Collections.singletonMap("me", luke));
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -163,7 +163,7 @@ public class GraphQlTesterTests {
.entityList(new ParameterizedTypeReference<MovieCharacter>() {})
.containsExactly(han, leia);
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -202,7 +202,7 @@ public class GraphQlTesterTests {
assertThatThrownBy(() -> this.graphQlTester.query(query).executeAndVerify())
.hasMessageContaining("Response has 1 unexpected error(s).");
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -214,7 +214,7 @@ public class GraphQlTesterTests {
assertThatThrownBy(() -> this.graphQlTester.query(query).execute().path("me"))
.hasMessageContaining("Response has 1 unexpected error(s).");
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -233,7 +233,7 @@ public class GraphQlTesterTests {
.verify())
.hasMessageContaining("Response has 1 unexpected error(s) of 2 total.");
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -252,7 +252,28 @@ public class GraphQlTesterTests {
.path("me")
.pathDoesNotExist();
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
void errorsFilteredGlobally() throws Exception {
String query = "{me {name, friends}}";
setResponse(
GraphqlErrorBuilder.newError().message("some error").build(),
GraphqlErrorBuilder.newError().message("some other error").build());
GraphQlTester.builder(this.service)
.errorFilter((error) -> error.getMessage().startsWith("some "))
.build()
.query(query)
.execute()
.errors()
.verify()
.path("me")
.pathDoesNotExist();
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
@Test
@@ -277,7 +298,7 @@ public class GraphQlTesterTests {
.path("me")
.pathDoesNotExist();
assertThat(getActualQuery()).contains(query);
assertThat(this.inputCaptor.getValue().getQuery()).contains(query);
}
private void setResponse(String data) throws Exception {
@@ -300,8 +321,4 @@ public class GraphQlTesterTests {
given(this.service.execute(this.inputCaptor.capture())).willReturn(Mono.just(result));
}
private String getActualQuery() {
return this.inputCaptor.getValue().getQuery();
}
}

View File

@@ -29,9 +29,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
@@ -60,8 +62,9 @@ import static org.mockito.Mockito.mock;
* </ul>
*
* <p>
* There is no actual handling via {@link graphql.GraphQL} in either scenario. The main
* focus is to verify {@link GraphQlTester} request preparation and response handling.
* There is no actual handling via {@link graphql.GraphQL} in either scenario.
* The main focus is to verify {@link GraphQlTester} request preparation and
* response handling.
*/
public class WebGraphQlTesterTests {
@@ -121,6 +124,28 @@ public class WebGraphQlTesterTests {
setup.shutdown();
}
@ParameterizedTest
@MethodSource("argumentSource")
void errorsFilteredGlobally(GraphQlTesterSetup setup) throws Exception {
String query = "{me {name, friends}}";
setup.response(
GraphqlErrorBuilder.newError().message("some error").build(),
GraphqlErrorBuilder.newError().message("some other error").build());
setup.graphQlTesterBuilder()
.errorFilter((error) -> error.getMessage().startsWith("some "))
.build()
.query(query)
.execute()
.errors()
.verify()
.path("me")
.pathDoesNotExist();
setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query));
}
private interface GraphQlTesterSetup {
@@ -243,8 +268,7 @@ public class WebGraphQlTesterTests {
public void response(@Nullable String data, List<GraphQLError> errors) throws Exception {
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
if (data != null) {
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {
}));
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {}));
}
if (!CollectionUtils.isEmpty(errors)) {
builder.addErrors(errors);