diff --git a/client/api/pom.xml b/client/api/pom.xml index ba854cc10..7aa5acda1 100644 --- a/client/api/pom.xml +++ b/client/api/pom.xml @@ -40,8 +40,8 @@ - jakarta.json - jakarta.json-api + com.fasterxml.jackson.core + jackson-databind provided diff --git a/client/api/src/main/java/io/smallrye/graphql/client/Request.java b/client/api/src/main/java/io/smallrye/graphql/client/Request.java index 40c9f5331..027bf982c 100644 --- a/client/api/src/main/java/io/smallrye/graphql/client/Request.java +++ b/client/api/src/main/java/io/smallrye/graphql/client/Request.java @@ -2,7 +2,7 @@ import java.util.Map; -import jakarta.json.JsonObject; +import com.fasterxml.jackson.databind.node.ObjectNode; public interface Request { String getDocument(); @@ -23,6 +23,6 @@ public interface Request { String toJson(); - JsonObject toJsonObject(); + ObjectNode toJsonObject(); } diff --git a/client/api/src/main/java/io/smallrye/graphql/client/Response.java b/client/api/src/main/java/io/smallrye/graphql/client/Response.java index beef05cd6..b77b41ded 100644 --- a/client/api/src/main/java/io/smallrye/graphql/client/Response.java +++ b/client/api/src/main/java/io/smallrye/graphql/client/Response.java @@ -3,17 +3,15 @@ import java.util.List; import java.util.Map; -import jakarta.json.JsonObject; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.node.ObjectNode; public interface Response { /** * The 'data' object contained in the response. - * Can be JsonValue.NULL if the response contains an empty field, or `null` if the response - * does not contain this field at all. + * Can be null if the response does not contain this field at all or contains a null value. */ - JsonObject getData(); + ObjectNode getData(); /** * List of errors contained in this response. @@ -23,7 +21,7 @@ public interface Response { /** * List of user-made extensions contained in this response. */ - JsonObject getExtensions(); + ObjectNode getExtensions(); /** * Transform the contents of the `rootField` from this response into a list of objects diff --git a/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeResponse.java b/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeResponse.java index 63f8c0322..81b594ee9 100644 --- a/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeResponse.java +++ b/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeResponse.java @@ -7,13 +7,13 @@ import java.util.Map; import java.util.Objects; -import jakarta.json.JsonObject; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.smallrye.graphql.client.GraphQLError; public final class TypesafeResponse extends ErrorOr { private Map> transportMeta; - private JsonObject extensions; + private ObjectNode extensions; public static TypesafeResponse of(T value) { return new TypesafeResponse<>(value, null); @@ -33,7 +33,7 @@ private TypesafeResponse(T value, List errors) { private TypesafeResponse(TypesafeResponse typesafeResponse, Map> transportMeta, - JsonObject extensions) { + ObjectNode extensions) { super( (typesafeResponse.isPresent()) ? typesafeResponse.get() : null, (typesafeResponse.hasErrors()) ? typesafeResponse.getErrors() : null); @@ -43,7 +43,7 @@ private TypesafeResponse(TypesafeResponse typesafeResponse, public static TypesafeResponse withTransportMetaAndExtensions(TypesafeResponse typesafeResponse, Map> transportMeta, - JsonObject responseExtensions) { + ObjectNode responseExtensions) { return new TypesafeResponse(typesafeResponse, transportMeta, responseExtensions); } @@ -56,9 +56,9 @@ public Map> getTransportMeta() { } /** - * Returns a JsonObject containing extensions to the GraphQL response, if any. + * Returns an ObjectNode containing extensions to the GraphQL response, if any. */ - public JsonObject getExtensions() { + public ObjectNode getExtensions() { return extensions; } diff --git a/client/implementation-vertx/pom.xml b/client/implementation-vertx/pom.xml index a1787e477..578134ba7 100644 --- a/client/implementation-vertx/pom.xml +++ b/client/implementation-vertx/pom.xml @@ -29,12 +29,16 @@ microprofile-config-api - jakarta.json - jakarta.json-api + com.fasterxml.jackson.core + jackson-databind - jakarta.json.bind - jakarta.json.bind-api + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + io.smallrye + smallrye-graphql-jackson-jsonb-compat @@ -46,11 +50,6 @@ io.vertx vertx-web-client - - org.eclipse - yasson - runtime - io.smallrye diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java index e7c1cd905..5a6e6f5a9 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java @@ -11,10 +11,10 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import jakarta.json.JsonObject; - import org.jboss.logging.Logger; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.Request; import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.core.Document; @@ -207,7 +207,7 @@ public Response executeSync(String query, Map variables, String return executeSync(buildRequest(query, variables, operationName).toJsonObject(), headers); } - private Response executeSync(JsonObject json, MultiMap additionalHeaders) { + private Response executeSync(ObjectNode json, MultiMap additionalHeaders) { if (executeSingleOperationsOverWebsocket) { return executeSingleResultOperationOverWebsocket(json).await().indefinitely(); } else { @@ -314,7 +314,7 @@ public Uni executeAsync(String query, Map variables, S return executeAsync(buildRequest(query, variables, operationName).toJsonObject(), headers); } - private Uni executeAsync(JsonObject json, MultiMap additionalHeaders) { + private Uni executeAsync(ObjectNode json, MultiMap additionalHeaders) { if (executeSingleOperationsOverWebsocket) { return executeSingleResultOperationOverWebsocket(json); } else { @@ -384,7 +384,7 @@ public Multi subscription(String query, Map variables, return subscription0(buildRequest(query, variables, operationName).toJsonObject()); } - private Multi subscription0(JsonObject json) { + private Multi subscription0(ObjectNode json) { return executeSubscriptionOverWebsocket(json); } @@ -447,7 +447,7 @@ private Uni webSocketHandler() { }); } - private Uni> executeSingleResultOperationOverHttp(JsonObject json, MultiMap allHeaders) { + private Uni> executeSingleResultOperationOverHttp(ObjectNode json, MultiMap allHeaders) { return url.get() .chain(instanceUrl -> Uni.createFrom().completionStage( webClient.postAbs(instanceUrl) @@ -461,7 +461,7 @@ private Response toResponse(HttpResponse httpResponse) { allowUnexpectedResponseFields); } - private Uni executeSingleResultOperationOverWebsocket(JsonObject json) { + private Uni executeSingleResultOperationOverWebsocket(ObjectNode json) { AtomicReference operationId = new AtomicReference<>(); AtomicReference handlerRef = new AtomicReference<>(); Uni rawUni = Uni.createFrom().emitter(rawEmitter -> { @@ -483,7 +483,7 @@ private Uni executeSingleResultOperationOverWebsocket(JsonObject json) .onItem().transform(data -> ResponseReader.readFrom(data, Collections.emptyMap())); } - private Multi executeSubscriptionOverWebsocket(JsonObject json) { + private Multi executeSubscriptionOverWebsocket(ObjectNode json) { AtomicReference operationId = new AtomicReference<>(); AtomicReference handlerRef = new AtomicReference<>(); Multi rawMulti = Multi.createFrom().emitter(rawEmitter -> { diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java index 635e74298..2c6921e10 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java @@ -1,6 +1,5 @@ package io.smallrye.graphql.client.vertx.typesafe; -import static io.smallrye.graphql.client.impl.JsonProviderHolder.JSON_PROVIDER; import static java.util.stream.Collectors.*; import java.lang.reflect.Array; @@ -23,16 +22,16 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; -import jakarta.json.JsonArray; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonValue; - import org.jboss.logging.Logger; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.InvalidResponseException; +import io.smallrye.graphql.client.impl.RequestImpl; import io.smallrye.graphql.client.impl.discovery.ServiceURLSupplier; import io.smallrye.graphql.client.impl.discovery.StaticURLSupplier; import io.smallrye.graphql.client.impl.discovery.StorkServiceURLSupplier; @@ -61,7 +60,8 @@ class VertxTypesafeGraphQLClientProxy { private static final Logger log = Logger.getLogger(VertxTypesafeGraphQLClientProxy.class); - private static final JsonBuilderFactory jsonObjectFactory = JSON_PROVIDER.createBuilderFactory(null); + private static final ObjectMapper MAPPER = RequestImpl.MAPPER; + private static final JsonNodeFactory NODES = JsonNodeFactory.instance; private final ConcurrentMap queryCache = new ConcurrentHashMap<>(); @@ -143,7 +143,7 @@ Object invoke(MethodInvocation method) { MultiMap headers = MultiMap.caseInsensitiveMultiMap() .addAll(new HeaderBuilder(api, method, additionalHeaders).build()); - JsonObject request = request(method); + ObjectNode request = request(method); if (method.getReturnType().isUni()) { if (executeSingleOperationsOverWebsocket) { @@ -162,7 +162,7 @@ Object invoke(MethodInvocation method) { } } - private Object executeSingleResultOperationOverHttpSync(MethodInvocation method, JsonObject request, MultiMap headers) { + private Object executeSingleResultOperationOverHttpSync(MethodInvocation method, ObjectNode request, MultiMap headers) { MultiMap allHeaders = MultiMap.caseInsensitiveMultiMap(); allHeaders.addAll(headers); // obtain values of dynamic headers and add them to the request @@ -178,7 +178,7 @@ private Object executeSingleResultOperationOverHttpSync(MethodInvocation method, allowUnexpectedResponseFields).read(); } - private Uni executeSingleResultOperationOverHttpAsync(MethodInvocation method, JsonObject request, + private Uni executeSingleResultOperationOverHttpAsync(MethodInvocation method, ObjectNode request, MultiMap headers) { return Uni.createFrom().deferred(() -> { // Fresh headers per (re)subscription @@ -221,7 +221,7 @@ private Map> convertHeaders(MultiMap input) { mapping(Map.Entry::getValue, toList()))); } - private Uni executeSingleResultOperationOverWebsocket(MethodInvocation method, JsonObject request) { + private Uni executeSingleResultOperationOverWebsocket(MethodInvocation method, ObjectNode request) { AtomicReference operationId = new AtomicReference<>(); AtomicReference handlerRef = new AtomicReference<>(); Uni rawUni = Uni.createFrom().emitter(rawEmitter -> { @@ -252,7 +252,7 @@ private Uni executeSingleResultOperationOverWebsocket(MethodInvocation m } - private Multi executeSubscriptionOverWebsocket(MethodInvocation method, JsonObject request) { + private Multi executeSubscriptionOverWebsocket(MethodInvocation method, ObjectNode request) { AtomicReference operationId = new AtomicReference<>(); AtomicReference handlerRef = new AtomicReference<>(); Multi rawMulti = Multi.createFrom().emitter(rawEmitter -> { @@ -309,33 +309,32 @@ private Uni webSocketHandler() { }); } - private JsonObject request(MethodInvocation method) { - JsonObjectBuilder request = jsonObjectFactory.createObjectBuilder(); + private ObjectNode request(MethodInvocation method) { + ObjectNode request = MAPPER.createObjectNode(); String query; if (clientModel == null) { query = queryCache.computeIfAbsent(method.getKey(), key -> new QueryBuilder(method).build()); } else { query = clientModel.getOperationMap().get(method.getMethodKey()); } - request.add("query", query); - request.add("variables", variables(method)); - request.add("operationName", method.getOperationName()); - JsonObject result = request.build(); - log.tracef("full graphql request: %s", result.toString()); - return result; + request.put("query", query); + request.set("variables", variables(method)); + request.put("operationName", method.getOperationName()); + log.tracef("full graphql request: %s", request.toString()); + return request; } - private JsonObjectBuilder variables(MethodInvocation method) { - JsonObjectBuilder builder = jsonObjectFactory.createObjectBuilder(); - method.valueParameters().forEach(parameter -> builder.add(parameter.getRawName(), value(parameter.getValue()))); + private ObjectNode variables(MethodInvocation method) { + ObjectNode builder = MAPPER.createObjectNode(); + method.valueParameters().forEach(parameter -> builder.set(parameter.getRawName(), value(parameter.getValue()))); return builder; } // TODO: the logic for serializing objects into JSON should probably be shared with server-side module // through a common module. Also this is not vert.x specific, another reason to move it out of this module - private JsonValue value(Object value) { + private JsonNode value(Object value) { if (value == null) { - return JsonValue.NULL; + return NODES.nullNode(); } TypeInfo type = TypeInfo.of(value.getClass()); if (type.isScalar()) { @@ -353,83 +352,83 @@ private JsonValue value(Object value) { return objectValue(value, type.fields()); } - private JsonValue scalarValue(Object value) { + private JsonNode scalarValue(Object value) { if (value instanceof String) { - return JSON_PROVIDER.createValue((String) value); + return NODES.textNode((String) value); } if (value instanceof java.sql.Date) { - return JSON_PROVIDER.createValue(value.toString()); + return NODES.textNode(value.toString()); } if (value instanceof Date) { - return JSON_PROVIDER.createValue(((Date) value).toInstant().toString()); + return NODES.textNode(((Date) value).toInstant().toString()); } if (value instanceof Calendar) { - return JSON_PROVIDER.createValue(((Calendar) value).toInstant().toString()); + return NODES.textNode(((Calendar) value).toInstant().toString()); } if (value instanceof Enum) { - return JSON_PROVIDER.createValue(((Enum) value).name()); + return NODES.textNode(((Enum) value).name()); } if (value instanceof Boolean) { - return ((Boolean) value) ? JsonValue.TRUE : JsonValue.FALSE; + return NODES.booleanNode((Boolean) value); } if (value instanceof Byte) { - return JSON_PROVIDER.createValue((Byte) value); + return NODES.numberNode((Byte) value); } if (value instanceof Short) { - return JSON_PROVIDER.createValue((Short) value); + return NODES.numberNode((Short) value); } if (value instanceof Integer) { - return JSON_PROVIDER.createValue((Integer) value); + return NODES.numberNode((Integer) value); } if (value instanceof Long) { - return JSON_PROVIDER.createValue((Long) value); + return NODES.numberNode((Long) value); } if (value instanceof Double) { - return JSON_PROVIDER.createValue((Double) value); + return NODES.numberNode((Double) value); } if (value instanceof Float) { - return JSON_PROVIDER.createValue((Float) value); + return NODES.numberNode((Float) value); } if (value instanceof BigInteger) { - return JSON_PROVIDER.createValue((BigInteger) value); + return NODES.numberNode((BigInteger) value); } if (value instanceof BigDecimal) { - return JSON_PROVIDER.createValue((BigDecimal) value); + return NODES.numberNode((BigDecimal) value); } if (value instanceof OptionalInt) { OptionalInt optionalValue = ((OptionalInt) value); - return ((optionalValue.isPresent()) ? JSON_PROVIDER.createValue(optionalValue.getAsInt()) : JsonValue.NULL); + return optionalValue.isPresent() ? NODES.numberNode(optionalValue.getAsInt()) : NODES.nullNode(); } if (value instanceof OptionalLong) { OptionalLong optionalValue = ((OptionalLong) value); - return ((optionalValue.isPresent()) ? JSON_PROVIDER.createValue(optionalValue.getAsLong()) : JsonValue.NULL); + return optionalValue.isPresent() ? NODES.numberNode(optionalValue.getAsLong()) : NODES.nullNode(); } if (value instanceof OptionalDouble) { OptionalDouble optionalValue = ((OptionalDouble) value); - return ((optionalValue.isPresent()) ? JSON_PROVIDER.createValue(optionalValue.getAsDouble()) : JsonValue.NULL); + return optionalValue.isPresent() ? NODES.numberNode(optionalValue.getAsDouble()) : NODES.nullNode(); } - return JSON_PROVIDER.createValue(value.toString()); + return NODES.textNode(value.toString()); } - private JsonArray arrayValue(Object value) { - JsonArrayBuilder array = jsonObjectFactory.createArrayBuilder(); + private ArrayNode arrayValue(Object value) { + ArrayNode array = MAPPER.createArrayNode(); values(value).forEach(item -> array.add(value(item))); - return array.build(); + return array; } - private JsonArray mapValue(Object value) { + private ArrayNode mapValue(Object value) { Map map = (Map) value; - JsonArrayBuilder array = jsonObjectFactory.createArrayBuilder(); + ArrayNode array = MAPPER.createArrayNode(); map.forEach((k, v) -> { - JsonObjectBuilder entryBuilder = jsonObjectFactory.createObjectBuilder(); - entryBuilder.add("key", value(k)); - entryBuilder.add("value", value(v)); - array.add(entryBuilder.build()); + ObjectNode entryNode = MAPPER.createObjectNode(); + entryNode.set("key", value(k)); + entryNode.set("value", value(v)); + array.add(entryNode); }); - return array.build(); + return array; } - private JsonValue optionalValue(Object value) { + private JsonNode optionalValue(Object value) { Optional optional = (Optional) value; return value(optional.orElse(null)); } @@ -454,14 +453,14 @@ private List primitiveArray(Object value) { return out; } - private JsonObject objectValue(Object object, Stream fields) { - JsonObjectBuilder builder = jsonObjectFactory.createObjectBuilder(); + private ObjectNode objectValue(Object object, Stream fields) { + ObjectNode builder = MAPPER.createObjectNode(); fields.forEach(field -> { if (field.isIncludeNull() || field.get(object) != null) { - builder.add(field.getName(), value(field.get(object))); + builder.set(field.getName(), value(field.get(object))); } }); - return builder.build(); + return builder; } private Uni> postAsync(String request, MultiMap headers) { diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/WebSocketSubprotocolHandler.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/WebSocketSubprotocolHandler.java index 1ea64a28b..07e81e7f1 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/WebSocketSubprotocolHandler.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/WebSocketSubprotocolHandler.java @@ -1,6 +1,6 @@ package io.smallrye.graphql.client.vertx.websocket; -import jakarta.json.JsonObject; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.MultiEmitter; @@ -30,7 +30,7 @@ public interface WebSocketSubprotocolHandler { * @param emitter Emitter that should receive the completion event (or an error) when the operation finishes. * @return The generated internal ID of this operation. */ - String executeUni(JsonObject request, UniEmitter emitter); + String executeUni(ObjectNode request, UniEmitter emitter); /** * Requests an execution of a subscription operation over the websocket. @@ -39,7 +39,7 @@ public interface WebSocketSubprotocolHandler { * @param emitter Emitter that should receive the completion events (or an error) from the subscription. * @return The generated internal ID of this operation. */ - String executeMulti(JsonObject request, MultiEmitter emitter); + String executeMulti(ObjectNode request, MultiEmitter emitter); /** * Cancels an active single-result operation with the given ID. This does not do anything with the Emitter for this diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java index 0c6da28cf..3bf86d7ca 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java @@ -1,32 +1,26 @@ package io.smallrye.graphql.client.vertx.websocket.graphqltransportws; -import static io.smallrye.graphql.client.impl.JsonProviderHolder.JSON_PROVIDER; - -import java.io.StringReader; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import jakarta.json.JsonArray; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import jakarta.json.stream.JsonParsingException; +import java.util.stream.StreamSupport; import org.jboss.logging.Logger; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.GraphQLClientException; import io.smallrye.graphql.client.GraphQLError; import io.smallrye.graphql.client.InvalidResponseException; import io.smallrye.graphql.client.UnexpectedCloseException; +import io.smallrye.graphql.client.impl.RequestImpl; import io.smallrye.graphql.client.impl.ResponseReader; import io.smallrye.graphql.client.vertx.websocket.WebSocketSubprotocolHandler; import io.smallrye.graphql.client.vertx.websocket.opid.IncrementingNumberOperationIDGenerator; @@ -44,13 +38,12 @@ public class GraphQLTransportWSSubprotocolHandler implements WebSocketSubprotocolHandler { private static final Logger log = Logger.getLogger(GraphQLTransportWSSubprotocolHandler.class); - private static final JsonBuilderFactory jsonBuilderFactory = JSON_PROVIDER.createBuilderFactory(null); - private static final JsonReaderFactory jsonReaderFactory = JSON_PROVIDER.createReaderFactory(null); + private static final ObjectMapper MAPPER = RequestImpl.MAPPER; private final Integer connectionInitializationTimeout; - private JsonObject connectionInitMessage; - private JsonObject pongMessage; + private ObjectNode connectionInitMessage; + private ObjectNode pongMessage; private final WebSocket webSocket; private final CompletableFuture initialization; @@ -89,14 +82,19 @@ private Uni initialize() { log.trace("Initializing websocket with graphql-transport-ws protocol"); } - JsonObjectBuilder payloadBuilder = jsonBuilderFactory.createObjectBuilder(); - if (!initPayload.isEmpty()) { - payloadBuilder.add("payload", jsonBuilderFactory.createObjectBuilder(initPayload)); + ObjectNode initNode = MAPPER.createObjectNode(); + initNode.put("type", "connection_init"); + if (!this.initPayload.isEmpty()) { + initNode.set("payload", MAPPER.valueToTree(this.initPayload)); } - connectionInitMessage = jsonBuilderFactory.createObjectBuilder().add("type", "connection_init") - .addAll(payloadBuilder).build(); - pongMessage = jsonBuilderFactory.createObjectBuilder().add("type", "pong") - .add("payload", jsonBuilderFactory.createObjectBuilder().add("message", "keepalive")).build(); + connectionInitMessage = initNode; + + ObjectNode pongNode = MAPPER.createObjectNode(); + pongNode.put("type", "pong"); + ObjectNode pongPayload = MAPPER.createObjectNode(); + pongPayload.put("message", "keepalive"); + pongNode.set("payload", pongPayload); + pongMessage = pongNode; webSocket.closeHandler((v) -> { onClose.run(); @@ -148,7 +146,7 @@ private Uni initialize() { log.trace("<<< " + text); } try { - JsonObject message = parseIncomingMessage(text); + ObjectNode message = parseIncomingMessage(text); MessageType messageType = getMessageType(message); switch (messageType) { case PING: @@ -162,20 +160,20 @@ private Uni initialize() { initializationEmitter.complete(null); break; case NEXT: - handleData(message.getString("id"), message.getJsonObject("payload")); + handleData(message.get("id").asText(), (ObjectNode) message.get("payload")); break; case ERROR: - handleOperationError(message.getString("id"), message.getJsonArray("payload")); + handleOperationError(message.get("id").asText(), (ArrayNode) message.get("payload")); break; case COMPLETE: - handleComplete(message.getString("id")); + handleComplete(message.get("id").asText()); break; case CONNECTION_INIT: case PONG: case SUBSCRIBE: break; } - } catch (JsonParsingException | IllegalArgumentException e) { + } catch (IllegalArgumentException e) { log.error("Unexpected message from server: " + text); // should we fail the operations here? } @@ -183,7 +181,7 @@ private Uni initialize() { }); } - private void handleData(String operationId, JsonObject data) { + private void handleData(String operationId, ObjectNode data) { // If this is a uni operation, we remove it right away from the active operation map, // even though we still should receive a 'complete' message later - we don't wait for it. // This is to prevent a potential memory leak in case that the server doesn't actually send it. @@ -207,8 +205,10 @@ private void handleData(String operationId, JsonObject data) { } } - private void handleOperationError(String operationId, JsonArray errors) { - List parsedErrors = errors.stream().map(ResponseReader::readError).collect(Collectors.toList()); + private void handleOperationError(String operationId, ArrayNode errors) { + List parsedErrors = StreamSupport.stream(errors.spliterator(), false) + .map(ResponseReader::readError) + .toList(); GraphQLClientException exception = new GraphQLClientException("Received an error", parsedErrors); UniEmitter emitter = uniOperations.remove(operationId); if (emitter != null) { @@ -255,22 +255,22 @@ private void failAllActiveOperationsWith(Throwable throwable) { } @Override - public String executeUni(JsonObject request, UniEmitter emitter) { + public String executeUni(ObjectNode request, UniEmitter emitter) { String id = operationIdGenerator.generate(); ensureInitialized().subscribe().with(ready -> { uniOperations.put(id, emitter); - JsonObject subscribe = createSubscribeMessage(request, id); + ObjectNode subscribe = createSubscribeMessage(request, id); send(webSocket, subscribe); }, emitter::fail); return id; } @Override - public String executeMulti(JsonObject request, MultiEmitter emitter) { + public String executeMulti(ObjectNode request, MultiEmitter emitter) { String id = operationIdGenerator.generate(); ensureInitialized().subscribe().with(ready -> { multiOperations.put(id, emitter); - JsonObject subscribe = createSubscribeMessage(request, id); + ObjectNode subscribe = createSubscribeMessage(request, id); send(webSocket, subscribe); }, emitter::fail); return id; @@ -295,43 +295,46 @@ public void close() { } } - private MessageType getMessageType(JsonObject message) { - return MessageType.fromString(message.getString("type")); + private MessageType getMessageType(ObjectNode message) { + return MessageType.fromString(message.get("type").asText()); } - private JsonObject parseIncomingMessage(String message) { - try (JsonReader jsonReader = jsonReaderFactory.createReader(new StringReader(message))) { - return jsonReader.readObject(); + private ObjectNode parseIncomingMessage(String message) { + try { + return (ObjectNode) MAPPER.readTree(message); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse incoming message", e); } } - private JsonObject createSubscribeMessage(JsonObject request, String id) { - JsonObjectBuilder payload = jsonBuilderFactory.createObjectBuilder(); + private ObjectNode createSubscribeMessage(ObjectNode request, String id) { + ObjectNode payload = MAPPER.createObjectNode(); - payload.add("query", request.getString("query")); - JsonValue operationName = request.get("operationName"); - if (operationName instanceof JsonString) { - payload.add("operationName", operationName); + payload.put("query", request.get("query").asText()); + JsonNode operationName = request.get("operationName"); + if (operationName != null && operationName.isTextual()) { + payload.set("operationName", operationName); } - JsonObject variables = request.getJsonObject("variables"); - if (variables != null) { - payload.add("variables", variables); + JsonNode variables = request.get("variables"); + if (variables != null && variables.isObject()) { + payload.set("variables", variables); } - return jsonBuilderFactory.createObjectBuilder() - .add("type", "subscribe") - .add("id", id) - .add("payload", payload) - .build(); + + ObjectNode msg = MAPPER.createObjectNode(); + msg.put("type", "subscribe"); + msg.put("id", id); + msg.set("payload", payload); + return msg; } - private JsonObject createCompleteMessage(String id) { - return jsonBuilderFactory.createObjectBuilder() - .add("type", "complete") - .add("id", id) - .build(); + private ObjectNode createCompleteMessage(String id) { + ObjectNode msg = MAPPER.createObjectNode(); + msg.put("type", "complete"); + msg.put("id", id); + return msg; } - private Uni send(WebSocket webSocket, JsonObject message) { + private Uni send(WebSocket webSocket, ObjectNode message) { String string = message.toString(); if (log.isTraceEnabled()) { log.trace(">>> " + string); diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqlws/GraphQLWSSubprotocolHandler.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqlws/GraphQLWSSubprotocolHandler.java index d095dd773..bed7f92da 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqlws/GraphQLWSSubprotocolHandler.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/websocket/graphqlws/GraphQLWSSubprotocolHandler.java @@ -1,29 +1,23 @@ package io.smallrye.graphql.client.vertx.websocket.graphqlws; -import static io.smallrye.graphql.client.impl.JsonProviderHolder.JSON_PROVIDER; - -import java.io.StringReader; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import jakarta.json.stream.JsonParsingException; - import org.jboss.logging.Logger; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.GraphQLClientException; import io.smallrye.graphql.client.GraphQLError; import io.smallrye.graphql.client.InvalidResponseException; import io.smallrye.graphql.client.UnexpectedCloseException; +import io.smallrye.graphql.client.impl.RequestImpl; import io.smallrye.graphql.client.impl.ResponseReader; import io.smallrye.graphql.client.vertx.websocket.WebSocketSubprotocolHandler; import io.smallrye.graphql.client.vertx.websocket.opid.IncrementingNumberOperationIDGenerator; @@ -41,8 +35,7 @@ public class GraphQLWSSubprotocolHandler implements WebSocketSubprotocolHandler { private static final Logger log = Logger.getLogger(GraphQLWSSubprotocolHandler.class); - private static final JsonBuilderFactory jsonBuilderFactory = JSON_PROVIDER.createBuilderFactory(null); - private static final JsonReaderFactory jsonReaderFactory = JSON_PROVIDER.createReaderFactory(null); + private static final ObjectMapper MAPPER = RequestImpl.MAPPER; private final Integer subscriptionInitializationTimeout; @@ -128,11 +121,12 @@ private Uni initialize() { log.trace("<<< " + text); } try { - JsonObject message = parseIncomingMessage(text); + ObjectNode message = parseIncomingMessage(text); MessageType messageType = getMessageType(message); switch (messageType) { case GQL_CONNECTION_ERROR: - failAllActiveOperationsWith(new InvalidResponseException(message.get("payload").toString())); + failAllActiveOperationsWith( + new InvalidResponseException(message.get("payload").toString())); webSocket.close(); break; case GQL_CONNECTION_ACK: @@ -142,13 +136,13 @@ private Uni initialize() { initializationEmitter.complete(null); break; case GQL_DATA: - handleData(message.getString("id"), message.getJsonObject("payload")); + handleData(message.get("id").asText(), (ObjectNode) message.get("payload")); break; case GQL_ERROR: - handleOperationError(message.getString("id"), message.getJsonObject("payload")); + handleOperationError(message.get("id").asText(), (ObjectNode) message.get("payload")); break; case GQL_COMPLETE: - handleComplete(message.getString("id")); + handleComplete(message.get("id").asText()); break; case GQL_START: case GQL_STOP: @@ -157,7 +151,7 @@ private Uni initialize() { case GQL_CONNECTION_TERMINATE: break; } - } catch (JsonParsingException | IllegalArgumentException e) { + } catch (IllegalArgumentException e) { log.error("Unexpected message from server: " + text); // should we fail the operations here? } @@ -171,22 +165,22 @@ public Uni ensureInitialized() { } @Override - public String executeUni(JsonObject request, UniEmitter emitter) { + public String executeUni(ObjectNode request, UniEmitter emitter) { String id = operationIdGenerator.generate(); ensureInitialized().subscribe().with(ready -> { uniOperations.put(id, emitter); - JsonObject subscribe = createSubscribeMessage(request, id); + ObjectNode subscribe = createSubscribeMessage(request, id); send(webSocket, subscribe); }, emitter::fail); return id; } @Override - public String executeMulti(JsonObject request, MultiEmitter emitter) { + public String executeMulti(ObjectNode request, MultiEmitter emitter) { String id = operationIdGenerator.generate(); ensureInitialized().subscribe().with(ready -> { multiOperations.put(id, emitter); - JsonObject subscribe = createSubscribeMessage(request, id); + ObjectNode subscribe = createSubscribeMessage(request, id); send(webSocket, subscribe); }, emitter::fail); return id; @@ -213,17 +207,19 @@ public void close() { } } - private JsonObject parseIncomingMessage(String message) { - try (JsonReader jsonReader = jsonReaderFactory.createReader(new StringReader(message))) { - return jsonReader.readObject(); + private ObjectNode parseIncomingMessage(String message) { + try { + return (ObjectNode) MAPPER.readTree(message); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse incoming message", e); } } - private MessageType getMessageType(JsonObject message) { - return MessageType.fromString(message.getString("type")); + private MessageType getMessageType(ObjectNode message) { + return MessageType.fromString(message.get("type").asText()); } - private Uni send(WebSocket webSocket, JsonObject message) { + private Uni send(WebSocket webSocket, ObjectNode message) { String string = message.toString(); if (log.isTraceEnabled()) { log.trace(">>> " + string); @@ -231,47 +227,46 @@ private Uni send(WebSocket webSocket, JsonObject message) { return Uni.createFrom().completionStage(webSocket.writeTextMessage(string).toCompletionStage()); } - private JsonObject createConnectionInitMessage() { - JsonObjectBuilder payloadBuilder = jsonBuilderFactory.createObjectBuilder(); + private ObjectNode createConnectionInitMessage() { + ObjectNode msg = MAPPER.createObjectNode(); + msg.put("type", "connection_init"); if (!initPayload.isEmpty()) { - payloadBuilder.add("payload", jsonBuilderFactory.createObjectBuilder(initPayload)); + msg.set("payload", MAPPER.valueToTree(initPayload)); } - return jsonBuilderFactory.createObjectBuilder() - .add("type", "connection_init") - .addAll(payloadBuilder) - .build(); + return msg; } - private JsonObject createStopMessage(String id) { - return jsonBuilderFactory.createObjectBuilder() - .add("type", "stop") - .add("id", id) - .build(); + private ObjectNode createStopMessage(String id) { + ObjectNode msg = MAPPER.createObjectNode(); + msg.put("type", "stop"); + msg.put("id", id); + return msg; } - private JsonObject createConnectionTerminateMessage() { - return jsonBuilderFactory.createObjectBuilder() - .add("type", "connection_terminate") - .build(); + private ObjectNode createConnectionTerminateMessage() { + ObjectNode msg = MAPPER.createObjectNode(); + msg.put("type", "connection_terminate"); + return msg; } - private JsonObject createSubscribeMessage(JsonObject request, String id) { - JsonObjectBuilder payload = jsonBuilderFactory.createObjectBuilder(); + private ObjectNode createSubscribeMessage(ObjectNode request, String id) { + ObjectNode payload = MAPPER.createObjectNode(); - payload.add("query", request.getString("query")); - JsonValue operationName = request.get("operationName"); - if (operationName instanceof JsonString) { - payload.add("operationName", operationName); + payload.put("query", request.get("query").asText()); + JsonNode operationName = request.get("operationName"); + if (operationName != null && operationName.isTextual()) { + payload.set("operationName", operationName); } - JsonObject variables = request.getJsonObject("variables"); - if (variables != null) { - payload.add("variables", variables); + JsonNode variables = request.get("variables"); + if (variables != null && variables.isObject()) { + payload.set("variables", variables); } - return jsonBuilderFactory.createObjectBuilder() - .add("type", "start") - .add("id", id) - .add("payload", payload) - .build(); + + ObjectNode msg = MAPPER.createObjectNode(); + msg.put("type", "start"); + msg.put("id", id); + msg.set("payload", payload); + return msg; } private void failAllActiveOperationsWith(Throwable throwable) { @@ -290,7 +285,7 @@ private void failAllActiveOperationsWith(Throwable throwable) { } } - private void handleData(String operationId, JsonObject data) { + private void handleData(String operationId, ObjectNode data) { // If this is a uni operation, we remove it right away from the active operation map, // even though we still should receive a 'complete' message later - we don't wait for it. // This is to prevent a potential memory leak in case that the server doesn't actually send it. @@ -314,7 +309,7 @@ private void handleData(String operationId, JsonObject data) { } } - private void handleOperationError(String operationId, JsonObject error) { + private void handleOperationError(String operationId, ObjectNode error) { GraphQLError parsedError = ResponseReader.readError(error); GraphQLClientException exception = new GraphQLClientException("Received an error", parsedError); UniEmitter emitter = uniOperations.remove(operationId); diff --git a/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/DynamicClientExceptionTest.java b/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/DynamicClientExceptionTest.java index 8a6123b9a..fda8ffa50 100644 --- a/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/DynamicClientExceptionTest.java +++ b/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/DynamicClientExceptionTest.java @@ -4,12 +4,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import jakarta.json.JsonObject; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.InvalidResponseException; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; @@ -35,7 +35,7 @@ public void test() throws ExecutionException, InterruptedException, TimeoutExcep .url("http://localhost:" + server.actualPort()) .build(); try { - JsonObject data = client.executeSync("{something-whatever}").getData(); + ObjectNode data = client.executeSync("{something-whatever}").getData(); Assertions.fail("Expected an exception"); } catch (Exception e) { Assertions.assertTrue(e instanceof InvalidResponseException); diff --git a/client/implementation-vertx/src/test/java/test/tck/VertxTypesafeGraphQLClientFixture.java b/client/implementation-vertx/src/test/java/test/tck/VertxTypesafeGraphQLClientFixture.java index fc0d34461..e489227d4 100644 --- a/client/implementation-vertx/src/test/java/test/tck/VertxTypesafeGraphQLClientFixture.java +++ b/client/implementation-vertx/src/test/java/test/tck/VertxTypesafeGraphQLClientFixture.java @@ -5,18 +5,17 @@ import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.verify; -import java.io.StringReader; import java.lang.reflect.Method; import java.net.URI; import java.util.List; import java.util.Map; -import jakarta.json.Json; -import jakarta.json.JsonObject; - import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.smallrye.graphql.client.typesafe.api.TypesafeGraphQLClientBuilder; import io.smallrye.graphql.client.vertx.typesafe.VertxTypesafeGraphQLClientBuilder; import io.vertx.core.MultiMap; @@ -35,7 +34,7 @@ public class VertxTypesafeGraphQLClientFixture implements TypesafeGraphQLClientF private Integer statusCode; private String statusMessage; private String response; - private JsonObject requestSent; + private JsonNode requestSent; private Map> transportMeta; public VertxTypesafeGraphQLClientFixture() { @@ -116,18 +115,19 @@ public String variables() { @Override public String rawVariables() { - JsonObject variables = requestSent().getJsonObject("variables"); + JsonNode variables = requestSent().get("variables"); return String.valueOf(variables); } @Override public String operationName() { - return requestSent().getString("operationName", "null"); + JsonNode operationName = requestSent().get("operationName"); + return operationName != null ? operationName.asText() : "null"; } @Override public String query() { - return requestSent().getString("query").replace('\"', '\''); + return requestSent().get("query").asText().replace('\"', '\''); } @Override @@ -135,13 +135,16 @@ public Map> transportMeta() { return transportMeta; } - private JsonObject requestSent() { + private JsonNode requestSent() { if (requestSent == null) { ArgumentCaptor captor = ArgumentCaptor.forClass(Buffer.class); then(mockHttpRequest).should().sendBuffer(captor.capture()); String requestString = captor.getValue().toString(); - requestSent = Json.createReader( - new StringReader(requestString)).readObject(); + try { + requestSent = new ObjectMapper().readTree(requestString); + } catch (Exception e) { + throw new RuntimeException("Failed to parse request JSON", e); + } } return requestSent; } diff --git a/client/implementation/pom.xml b/client/implementation/pom.xml index 07f9913d5..27a700273 100644 --- a/client/implementation/pom.xml +++ b/client/implementation/pom.xml @@ -67,9 +67,20 @@ provided - jakarta.json - jakarta.json-api - provided + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + io.smallrye + smallrye-graphql-jackson-jsonb-compat jakarta.json.bind @@ -135,10 +146,5 @@ ${version.mockito} test - - org.eclipse - yasson - test - diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/JsonProviderHolder.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/JsonProviderHolder.java deleted file mode 100644 index ea4a041da..000000000 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/JsonProviderHolder.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.smallrye.graphql.client.impl; - -import jakarta.json.spi.JsonProvider; - -// A central place to get the JsonProvider to avoid calling `JsonProvider.provider()` many times -// due to associated performance costs. -public class JsonProviderHolder { - - public static final JsonProvider JSON_PROVIDER = JsonProvider.provider(); - -} diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java index 558d2523a..3ad4003c7 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java @@ -1,24 +1,24 @@ package io.smallrye.graphql.client.impl; -import static io.smallrye.graphql.client.impl.JsonProviderHolder.JSON_PROVIDER; - -import java.io.StringReader; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonValue; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.smallrye.graphql.client.Request; +import io.smallrye.graphql.jackson.jsonb.JsonbCompatModule; public class RequestImpl implements Request { - private static final JsonBuilderFactory JSON = JSON_PROVIDER.createBuilderFactory(null); - private static final Jsonb JSONB = JsonbBuilder.create(); + public static final ObjectMapper MAPPER = JsonMapper.builder() + .addModule(new JsonbCompatModule()) + .enable(com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .disable(com.fasterxml.jackson.databind.cfg.JsonNodeFeature.STRIP_TRAILING_BIGDECIMAL_ZEROES) + .build(); private final String document; private Map variables; @@ -76,64 +76,41 @@ public RequestImpl build() { @Override public String toJson() { - JsonObjectBuilder queryBuilder = JSON.createObjectBuilder().add("query", document); - if (!variables.isEmpty()) { - queryBuilder.add("variables", _formatJsonVariables()); - } - if (operationName != null && !operationName.isEmpty()) { - queryBuilder.add("operationName", operationName); - } - if (extensions != null && !extensions.isEmpty()) { - queryBuilder.add("extensions", _formatJsonMap(extensions)); + try { + return MAPPER.writeValueAsString(toJsonObject()); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize request to JSON", e); } - return queryBuilder.build().toString(); } @Override - public JsonObject toJsonObject() { - JsonObjectBuilder queryBuilder = JSON.createObjectBuilder().add("query", document); + public ObjectNode toJsonObject() { + ObjectNode node = MAPPER.createObjectNode(); + node.put("query", document); if (!variables.isEmpty()) { - queryBuilder.add("variables", _formatJsonVariables()); + node.set("variables", _formatJsonMap(variables)); } if (operationName != null && !operationName.isEmpty()) { - queryBuilder.add("operationName", operationName); + node.put("operationName", operationName); } if (extensions != null && !extensions.isEmpty()) { - queryBuilder.add("extensions", _formatJsonMap(extensions)); + node.set("extensions", _formatJsonMap(extensions)); } - return queryBuilder.build(); - } - - private JsonObject _formatJsonVariables() { - return _formatJsonMap(variables); + return node; } - private static JsonObject _formatJsonMap(Map map) { - JsonObjectBuilder builder = JSON.createObjectBuilder(); - + private ObjectNode _formatJsonMap(Map map) { + ObjectNode node = MAPPER.createObjectNode(); map.forEach((k, v) -> { - if (v instanceof String) { - builder.add(k, (String) v); - } else if (v instanceof Integer) { - builder.add(k, (Integer) v); - } else if (v instanceof JsonValue) { - builder.add(k, (JsonValue) v); - } else if (v instanceof Boolean) { - builder.add(k, (Boolean) v); - } else if (v instanceof Long) { - builder.add(k, (Long) v); - } else if (v instanceof Double) { - builder.add(k, (Double) v); - } else if (v instanceof Enum) { - builder.add(k, ((Enum) v).name()); - } else if (v == null) { - builder.addNull(k); + if (v == null) { + node.putNull(k); + } else if (v instanceof JsonNode) { + node.set(k, (JsonNode) v); } else { - builder.add(k, JSON_PROVIDER.createReader(new StringReader(JSONB.toJson(v))).read()); + node.set(k, MAPPER.valueToTree(v)); } }); - - return builder.build(); + return node; } @Override diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseImpl.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseImpl.java index d17f95a53..b9b6bd0d8 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseImpl.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseImpl.java @@ -3,14 +3,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; -import jakarta.json.JsonArray; -import jakarta.json.JsonNumber; -import jakarta.json.JsonObject; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.smallrye.graphql.client.GraphQLClientException; import io.smallrye.graphql.client.GraphQLError; @@ -23,18 +23,18 @@ public class ResponseImpl implements Response { public static final String STATUS_CODE = ""; public static final String STATUS_MESSAGE = ""; - private final JsonObject data; + private final ObjectNode data; private final List errors; - private final JsonObject extensions; + private final ObjectNode extensions; private final Map> metadata; - public ResponseImpl(JsonObject data, List errors, Map> headers) { + public ResponseImpl(ObjectNode data, List errors, Map> headers) { this(data, errors, null, headers); } - public ResponseImpl(JsonObject data, + public ResponseImpl(ObjectNode data, List errors, - JsonObject extensions, + ObjectNode extensions, Map> headers) { this.data = data; this.errors = errors; @@ -42,12 +42,12 @@ public ResponseImpl(JsonObject data, this.metadata = Collections.unmodifiableMap(headers != null ? headers : Collections.emptyMap()); } - public ResponseImpl(JsonObject data, List errors, Map> headers, + public ResponseImpl(ObjectNode data, List errors, Map> headers, Integer statusCode, String statusMessage) { this(data, errors, null, headers, statusCode, statusMessage); } - public ResponseImpl(JsonObject data, List errors, JsonObject extensions, + public ResponseImpl(ObjectNode data, List errors, ObjectNode extensions, Map> headers, Integer statusCode, String statusMessage) { this.data = data; @@ -68,53 +68,53 @@ public ResponseImpl(JsonObject data, List errors, JsonObject exten @Override public T getObject(Class dataType, String rootField) { - if (data == null || data.equals(JsonValue.NULL) || data.keySet().isEmpty()) { + if (data == null || data.isNull() || data.isEmpty()) { throw SmallRyeGraphQLClientMessages.msg.noDataInResponse(); } - JsonValue value = data.get(rootField); + JsonNode value = data.get(rootField); if (value == null) { // field is missing in the response completely - throw SmallRyeGraphQLClientMessages.msg.fieldNotFoundInResponse(rootField, data.keySet()); + throw SmallRyeGraphQLClientMessages.msg.fieldNotFoundInResponse(rootField, fieldNames(data)); } - if (value.getValueType().equals(JsonValue.ValueType.NULL)) { + if (value.isNull()) { // field is present in the response, but is null return null; } - if (value.getValueType().equals(JsonValue.ValueType.OBJECT)) { - return (T) JsonReader.readJson(rootField, TypeInfo.of(dataType), value.asJsonObject(), null); - } else if (value.getValueType().equals(JsonValue.ValueType.ARRAY)) { + if (value.isObject()) { + return (T) JsonReader.readJson(rootField, TypeInfo.of(dataType), value, null); + } else if (value.isArray()) { throw SmallRyeGraphQLClientMessages.msg.responseContainsArray(rootField); } else { Object scalarValue = getScalarValue(value); if (scalarValue != null) { return (T) scalarValue; } - throw SmallRyeGraphQLClientMessages.msg.unexpectedValueInResponse(rootField, value.getValueType().toString()); + throw SmallRyeGraphQLClientMessages.msg.unexpectedValueInResponse(rootField, value.getNodeType().toString()); } } @Override public List getList(Class dataType, String rootField) { - if (data == null || data.equals(JsonValue.NULL) || data.keySet().isEmpty()) { + if (data == null || data.isNull() || data.isEmpty()) { throw SmallRyeGraphQLClientMessages.msg.noDataInResponse(); } - JsonValue item = data.get(rootField); + JsonNode item = data.get(rootField); if (item == null) { - throw SmallRyeGraphQLClientMessages.msg.fieldNotFoundInResponse(rootField, data.keySet()); + throw SmallRyeGraphQLClientMessages.msg.fieldNotFoundInResponse(rootField, fieldNames(data)); } - if (item.getValueType().equals(JsonValue.ValueType.NULL)) { + if (item.isNull()) { // field is present in the response, but is null return null; } - if (item instanceof JsonObject) { + if (item.isObject()) { throw SmallRyeGraphQLClientMessages.msg.responseContainsSingleObject(rootField); } - if (item instanceof JsonArray) { + if (item.isArray()) { List result = new ArrayList<>(); - JsonArray jsonArray = (JsonArray) item; + ArrayNode jsonArray = (ArrayNode) item; TypeInfo type = TypeInfo.of(dataType); jsonArray.forEach(o -> { - if (o.getValueType().equals(JsonValue.ValueType.OBJECT)) { + if (o.isObject()) { result.add((T) JsonReader.readJson(rootField, type, o, null)); } else { result.add((T) getScalarValue(o)); @@ -122,26 +122,34 @@ public List getList(Class dataType, String rootField) { }); return result; } - throw SmallRyeGraphQLClientMessages.msg.unexpectedValueInResponse(rootField, item.getValueType().toString()); + throw SmallRyeGraphQLClientMessages.msg.unexpectedValueInResponse(rootField, item.getNodeType().toString()); } - private Object getScalarValue(JsonValue value) { - switch (value.getValueType()) { + private Object getScalarValue(JsonNode value) { + switch (value.getNodeType()) { case NUMBER: - return ((JsonNumber) value).longValue(); + return value.longValue(); case STRING: - return ((JsonString) value).getString(); - case TRUE: - return true; - case FALSE: - return false; + return value.asText(); + case BOOLEAN: + return value.booleanValue(); default: return null; } } + /** + * Helper to collect field names from an ObjectNode into a Set-like structure + * for error reporting. + */ + private static Set fieldNames(ObjectNode node) { + Set names = new LinkedHashSet<>(); + node.fieldNames().forEachRemaining(names::add); + return names; + } + @Override - public JsonObject getData() { + public ObjectNode getData() { return data; } @@ -151,7 +159,7 @@ public List getErrors() { } @Override - public JsonObject getExtensions() { + public ObjectNode getExtensions() { return extensions; } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseReader.java index 6b1504c7c..e4dd866a8 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/ResponseReader.java @@ -1,30 +1,25 @@ package io.smallrye.graphql.client.impl; -import static io.smallrye.graphql.client.impl.JsonProviderHolder.JSON_PROVIDER; - -import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import jakarta.json.JsonArray; -import jakarta.json.JsonNumber; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; - import org.jboss.logging.Logger; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.GraphQLError; import io.smallrye.graphql.client.InvalidResponseException; public class ResponseReader { private static final Logger LOG = Logger.getLogger(ResponseReader.class.getName()); - private static final JsonReaderFactory jsonReaderFactory = JSON_PROVIDER.createReaderFactory(null); + private static final ObjectMapper MAPPER = RequestImpl.MAPPER; /** * Parse a GraphQL response from the input string. @@ -33,21 +28,20 @@ public class ResponseReader { * at least one of the keys 'data', 'extensions' and 'errors', but NO OTHER key. * (see https://spec.graphql.org/draft/#sec-Response-Format) */ - public static JsonObject parseGraphQLResponse(String input) { + public static ObjectNode parseGraphQLResponse(String input) { return parseGraphQLResponse(input, false); } - public static JsonObject parseGraphQLResponse(String input, Boolean allowUnexpectedResponseFields) { + public static ObjectNode parseGraphQLResponse(String input, Boolean allowUnexpectedResponseFields) { if (input == null) { return null; } - try (JsonReader jsonReader = jsonReaderFactory.createReader(new StringReader(input))) { - JsonObject jsonResponse; - try { - jsonResponse = jsonReader.readObject(); - } catch (Exception e) { + try { + JsonNode parsed = MAPPER.readTree(input); + if (!parsed.isObject()) { return null; } + ObjectNode jsonResponse = (ObjectNode) parsed; // validate that this is what we consider a GraphQL response - else return null if (jsonResponse.size() >= 1) { @@ -56,12 +50,16 @@ public static JsonObject parseGraphQLResponse(String input, Boolean allowUnexpec } else { return null; } + } catch (JsonProcessingException e) { + return null; } } - private static JsonObject checkExpectedResponseFields(JsonObject jsonResponse, + private static ObjectNode checkExpectedResponseFields(ObjectNode jsonResponse, Boolean allowUnexpectedResponseFields) { - for (String key : jsonResponse.keySet()) { + var fieldNames = jsonResponse.fieldNames(); + while (fieldNames.hasNext()) { + String key = fieldNames.next(); if (!key.equalsIgnoreCase("data") && !key.equalsIgnoreCase("errors") && !key.equalsIgnoreCase("extensions")) { @@ -75,40 +73,41 @@ private static JsonObject checkExpectedResponseFields(JsonObject jsonResponse, public static ResponseImpl readFrom(String input, Map> headers, Integer statusCode, String statusMessage, Boolean allowUnexpectedResponseFields) { - JsonObject jsonResponse = parseGraphQLResponse(input, allowUnexpectedResponseFields); + ObjectNode jsonResponse = parseGraphQLResponse(input, allowUnexpectedResponseFields); if (jsonResponse == null) { throw new InvalidResponseException( "Unexpected response. Code=" + statusCode + ", message=\"" + statusMessage + "\", " + "body=\"" + input + "\"", null, headers); } - JsonObject data = null; - if (jsonResponse.containsKey("data")) { - if (!jsonResponse.isNull("data")) { - data = jsonResponse.getJsonObject("data"); + ObjectNode data = null; + if (jsonResponse.has("data")) { + JsonNode dataNode = jsonResponse.get("data"); + if (!dataNode.isNull()) { + data = (ObjectNode) dataNode; } else { SmallRyeGraphQLClientLogging.log.noDataInResponse(); } } List errors = null; - if (jsonResponse.containsKey("errors")) { + if (jsonResponse.has("errors")) { errors = new ArrayList<>(); - for (JsonValue error : jsonResponse.getJsonArray("errors")) { + for (JsonNode error : jsonResponse.get("errors")) { errors.add(readError(error)); } } - boolean containsKeyExtension = jsonResponse.containsKey("extensions"); + boolean containsKeyExtension = jsonResponse.has("extensions"); - if (containsKeyExtension && !jsonResponse.isNull("extensions") - && !jsonResponse.get("extensions").getValueType().equals(JsonValue.ValueType.OBJECT)) { + if (containsKeyExtension && !jsonResponse.get("extensions").isNull() + && !jsonResponse.get("extensions").isObject()) { LOG.warn("Unexpected value of 'extensions' in response: " + jsonResponse.get("extensions")); } - JsonObject extensions = null; - if (containsKeyExtension && jsonResponse.get("extensions").getValueType().equals(JsonValue.ValueType.OBJECT)) { - extensions = jsonResponse.getJsonObject("extensions"); + ObjectNode extensions = null; + if (containsKeyExtension && jsonResponse.get("extensions").isObject()) { + extensions = (ObjectNode) jsonResponse.get("extensions"); } return new ResponseImpl(data, errors, extensions, headers, statusCode, statusMessage); @@ -123,31 +122,31 @@ public static ResponseImpl readFrom(String input, Map> head return readFrom(input, headers, statusCode, statusMessage, false); } - public static GraphQLError readError(JsonValue errorJson) { - JsonObject errorObject = errorJson.asJsonObject(); + public static GraphQLError readError(JsonNode errorJson) { + ObjectNode errorObject = (ObjectNode) errorJson; GraphQLErrorImpl decodedError = new GraphQLErrorImpl(); try { - JsonValue message = errorObject.get("message"); - if (message instanceof JsonString) { - decodedError.setMessage(errorObject.getString("message")); + JsonNode message = errorObject.get("message"); + if (message != null && message.isTextual()) { + decodedError.setMessage(message.asText()); } } catch (RuntimeException e) { LOG.warn(e); } try { - if (errorObject.containsKey("locations") - && errorObject.get("locations").getValueType().equals(JsonValue.ValueType.ARRAY)) { - JsonArray locations = errorObject.getJsonArray("locations"); + if (errorObject.has("locations") + && errorObject.get("locations").isArray()) { + ArrayNode locations = (ArrayNode) errorObject.get("locations"); List> locationList = new ArrayList<>(); - for (JsonValue jsonValue : locations) { - JsonObject location = jsonValue.asJsonObject(); + for (JsonNode jsonValue : locations) { + ObjectNode location = (ObjectNode) jsonValue; Map map = new HashMap<>(); - location.forEach((key, value) -> { + location.fields().forEachRemaining(entry -> { // TODO: how to handle non-numeric location segments? - if (value instanceof JsonNumber) { - map.put(key, ((JsonNumber) value).intValue()); + if (entry.getValue().isNumber()) { + map.put(entry.getKey(), entry.getValue().intValue()); } }); locationList.add(map); @@ -159,20 +158,21 @@ public static GraphQLError readError(JsonValue errorJson) { } try { - if (errorObject.containsKey("path") - && errorObject.get("path").getValueType().equals(JsonValue.ValueType.ARRAY)) { - Object[] path = new Object[errorObject.getJsonArray("path").size()]; + if (errorObject.has("path") + && errorObject.get("path").isArray()) { + ArrayNode pathArray = (ArrayNode) errorObject.get("path"); + Object[] path = new Object[pathArray.size()]; int i = 0; - for (JsonValue segment : errorObject.getJsonArray("path")) { - switch (segment.getValueType()) { + for (JsonNode segment : pathArray) { + switch (segment.getNodeType()) { case STRING: - path[i] = ((JsonString) segment).getString(); + path[i] = segment.asText(); break; case NUMBER: - path[i] = ((JsonNumber) segment).intValue(); + path[i] = segment.intValue(); break; default: - SmallRyeGraphQLClientLogging.log.unknownPathSegmentType(segment.getValueType()); + SmallRyeGraphQLClientLogging.log.unknownPathSegmentType(segment.getNodeType()); } i++; } @@ -183,12 +183,12 @@ public static GraphQLError readError(JsonValue errorJson) { } try { - if (errorObject.containsKey("extensions") - && errorObject.get("extensions").getValueType().equals(JsonValue.ValueType.OBJECT)) { - JsonObject extensions = errorObject.getJsonObject("extensions"); + if (errorObject.has("extensions") + && errorObject.get("extensions").isObject()) { + ObjectNode extensions = (ObjectNode) errorObject.get("extensions"); Map extensionMap = new HashMap<>(); - extensions.forEach((key, value) -> { - extensionMap.put(key, decode(value)); + extensions.fields().forEachRemaining(entry -> { + extensionMap.put(entry.getKey(), decode(entry.getValue())); }); decodedError.setExtensions(extensionMap); } else { @@ -201,14 +201,14 @@ public static GraphQLError readError(JsonValue errorJson) { try { // check if there are any other fields beyond the ones described by the specification Map otherFields = new HashMap<>(); - for (String key : errorObject.keySet()) { + errorObject.fieldNames().forEachRemaining(key -> { if (!key.equals("extensions") && !key.equals("locations") && !key.equals("message") && !key.equals("path")) { otherFields.put(key, decode(errorObject.get(key))); } - } + }); if (!otherFields.isEmpty()) { decodedError.setOtherFields(otherFields); } @@ -219,16 +219,14 @@ public static GraphQLError readError(JsonValue errorJson) { return decodedError; } - private static Object decode(JsonValue value) { - switch (value.getValueType()) { + private static Object decode(JsonNode value) { + switch (value.getNodeType()) { case STRING: - return ((JsonString) value).getString(); + return value.asText(); case NUMBER: - return ((JsonNumber) value).longValue(); - case FALSE: - return false; - case TRUE: - return true; + return value.longValue(); + case BOOLEAN: + return value.booleanValue(); case NULL: return null; default: diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/SmallRyeGraphQLClientLogging.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/SmallRyeGraphQLClientLogging.java index 190e302f2..89d23e008 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/SmallRyeGraphQLClientLogging.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/SmallRyeGraphQLClientLogging.java @@ -2,13 +2,13 @@ import static java.lang.invoke.MethodHandles.lookup; -import jakarta.json.JsonValue; - import org.jboss.logging.Logger; import org.jboss.logging.annotations.LogMessage; import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; +import com.fasterxml.jackson.databind.node.JsonNodeType; + @MessageLogger(projectCode = "SRGQL") public interface SmallRyeGraphQLClientLogging { @@ -28,10 +28,10 @@ public interface SmallRyeGraphQLClientLogging { @LogMessage(level = Logger.Level.WARN) @Message(id = 30002, value = "Unsupported path segment type %s (expecting only String or Number), ignoring") - void unknownPathSegmentType(JsonValue.ValueType type); + void unknownPathSegmentType(JsonNodeType type); @LogMessage(level = Logger.Level.WARN) @Message(id = 30003, value = "Unsupported extension type %s, ignoring") - void unknownExtensionType(JsonValue.ValueType valueType); + void unknownExtensionType(JsonNodeType nodeType); } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java index 8506b2411..4bf2c6cd0 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java @@ -1,25 +1,21 @@ package io.smallrye.graphql.client.impl.typesafe; -import static io.smallrye.graphql.client.impl.JsonProviderHolder.JSON_PROVIDER; import static io.smallrye.graphql.client.impl.typesafe.json.JsonUtils.isListOf; -import static jakarta.json.stream.JsonCollectors.toJsonArray; import static java.util.stream.Collectors.joining; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -import jakarta.json.JsonArray; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonException; -import jakarta.json.JsonObject; -import jakarta.json.JsonPatch; -import jakarta.json.JsonPointer; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.smallrye.graphql.client.GraphQLClientException; import io.smallrye.graphql.client.InvalidResponseException; +import io.smallrye.graphql.client.impl.RequestImpl; import io.smallrye.graphql.client.impl.ResponseReader; import io.smallrye.graphql.client.impl.typesafe.json.JsonReader; import io.smallrye.graphql.client.impl.typesafe.json.JsonUtils; @@ -28,15 +24,15 @@ import io.smallrye.graphql.client.typesafe.api.TypesafeResponse; public class ResultBuilder { - private static final JsonBuilderFactory jsonBuilderFactory = JSON_PROVIDER.createBuilderFactory(null); + private static final ObjectMapper MAPPER = RequestImpl.MAPPER; private final MethodInvocation method; - private final JsonObject response; + private final ObjectNode response; private final String responseString; private final Integer statusCode; private final String statusMessage; - private JsonObject data; - private JsonObject extensions; + private ObjectNode data; + private ObjectNode extensions; private Map> transportMeta; public ResultBuilder(MethodInvocation method, String responseString, boolean allowUnexpectedResponseFields) { @@ -67,7 +63,7 @@ public Object read() { readErrors(); if (data == null) return null; - JsonValue value = method.isSingle() ? data.get(method.getName()) : data; + JsonNode value = method.isSingle() ? data.get(method.getName()) : data; Object result; if (method.getReturnType().isTypesafeResponse()) { extensions = readExtensions(); @@ -89,75 +85,105 @@ public Object read() { return result; } - private JsonObject readData() { - if (!response.containsKey("data") || response.isNull("data")) + private ObjectNode readData() { + if (!response.has("data") || response.get("data").isNull()) return null; - JsonObject data = response.getJsonObject("data"); + ObjectNode data = (ObjectNode) response.get("data"); for (String namespace : method.getNamespaces()) { - data = data.getJsonObject(namespace); + data = (ObjectNode) data.get(namespace); } - if (method.isSingle() && !data.containsKey(method.getName())) + if (method.isSingle() && !data.has(method.getName())) throw new InvalidResponseException("No data for '" + method.getName() + "'"); return data; } private void readErrors() { - if (!response.containsKey("errors") || response.isNull("errors")) + if (!response.has("errors") || response.get("errors").isNull()) return; - JsonArray jsonErrors = response.getJsonArray("errors"); - if (jsonErrors == null) + JsonNode errorsNode = response.get("errors"); + if (!errorsNode.isArray()) return; - JsonArray unapplied = jsonErrors.stream().filter(error -> !apply(error)).collect(toJsonArray()); + ArrayNode jsonErrors = (ArrayNode) errorsNode; + ArrayNode unapplied = MAPPER.createArrayNode(); + for (JsonNode error : jsonErrors) { + if (!apply(error)) { + unapplied.add(error); + } + } if (unapplied.isEmpty()) return; throw new GraphQLClientException("errors from service", - unapplied.stream().map(ResponseReader::readError).collect(Collectors.toList())); + StreamSupport.stream(unapplied.spliterator(), false) + .map(ResponseReader::readError) + .toList()); } - private boolean apply(JsonValue error) { + private boolean apply(JsonNode error) { List path = getPath(error); if (data == null || path == null) return false; - JsonPointer pointer = JSON_PROVIDER.createPointer(path.stream().map(Object::toString).collect(joining("/", "/", ""))); - if (!exists(pointer)) + String pointerString = path.stream().map(Object::toString).collect(joining("/", "/", "")); + JsonPointer pointer = JsonPointer.compile(pointerString); + JsonNode existing = data.at(pointer); + if (existing.isMissingNode()) return false; - JsonArrayBuilder errors = jsonBuilderFactory.createArrayBuilder(); - if (pointer.containsValue(data) && isListOf(pointer.getValue(data), ErrorOr.class.getSimpleName())) - pointer.getValue(data).asJsonArray().forEach(errors::add); - errors.add(ERROR_MARK.apply((JsonObject) error)); - this.data = pointer.replace(data, errors.build()); + + // Build the error-marked array + ArrayNode errors = MAPPER.createArrayNode(); + if (isListOf(existing, ErrorOr.class.getSimpleName())) { + for (JsonNode e : existing) { + errors.add(e); + } + } + // Add __typename to error object to mark it + ObjectNode errorCopy = ((ObjectNode) error).deepCopy(); + errorCopy.put("__typename", ErrorOr.class.getSimpleName()); + errors.add(errorCopy); + + // Set the error array at the pointer location in data + setAtPointer(data, pointer, errors); return true; } - private JsonObject readExtensions() { - if (!response.containsKey("extensions") || response.isNull("extensions")) - return null; - return response.getJsonObject("extensions"); - } + /** + * Set a value at a given JsonPointer location in the tree. + */ + private void setAtPointer(ObjectNode root, JsonPointer pointer, JsonNode newValue) { + // Navigate to the parent and set the value + JsonPointer head = pointer.head(); + String lastSegment = pointer.last().getMatchingProperty(); + + JsonNode parent; + if (head != null && !head.toString().isEmpty()) { + parent = root.at(head); + } else { + parent = root; + } - private boolean exists(JsonPointer pointer) { - try { - pointer.containsValue(data); - return true; - } catch (JsonException e) { - return false; + if (parent.isObject()) { + ((ObjectNode) parent).set(lastSegment, newValue); + } else if (parent.isArray()) { + int index = Integer.parseInt(lastSegment); + ((ArrayNode) parent).set(index, newValue); } } - private static List getPath(JsonValue jsonValue) { - JsonValue value = jsonValue.asJsonObject().get("path"); - JsonArray jsonArray; - if (value != null && value.getValueType().equals(JsonValue.ValueType.ARRAY)) { - jsonArray = value.asJsonArray(); - } else { - jsonArray = null; - } - return (jsonArray == null) ? null : jsonArray.stream().map(JsonUtils::toValue).collect(Collectors.toList()); + private ObjectNode readExtensions() { + if (!response.has("extensions") || response.get("extensions").isNull()) + return null; + return (ObjectNode) response.get("extensions"); } - private static final JsonPatch ERROR_MARK = JSON_PROVIDER.createPatchBuilder() - .add("/__typename", ErrorOr.class.getSimpleName()) - .build(); + private static List getPath(JsonNode jsonValue) { + JsonNode value = jsonValue.get("path"); + if (value == null || !value.isArray()) { + return null; + } + ArrayNode jsonArray = (ArrayNode) value; + return StreamSupport.stream(jsonArray.spliterator(), false) + .map(JsonUtils::toValue) + .toList(); + } } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonArrayReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonArrayReader.java index 5c6274b59..f3e127e51 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonArrayReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonArrayReader.java @@ -7,22 +7,22 @@ import java.util.List; import java.util.Set; import java.util.stream.Collector; +import java.util.stream.StreamSupport; -import jakarta.json.JsonArray; -import jakarta.json.JsonValue; -import jakarta.json.JsonValue.ValueType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import io.smallrye.graphql.client.InvalidResponseException; import io.smallrye.graphql.client.impl.typesafe.CollectionUtils; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -class JsonArrayReader extends Reader { +class JsonArrayReader extends Reader { private Class collectionType; private TypeInfo itemType; - JsonArrayReader(TypeInfo type, Location location, JsonArray value, FieldInfo field) { + JsonArrayReader(TypeInfo type, Location location, ArrayNode value, FieldInfo field) { super(type, location, value, field); } @@ -30,13 +30,15 @@ class JsonArrayReader extends Reader { Object read() { GraphQLClientValueHelper.check(location, value, type.isCollection()); IndexedLocationBuilder locationBuilder = new IndexedLocationBuilder(location); - return value.stream().map(item -> readItem(locationBuilder, item)).collect(collector()); + return StreamSupport.stream(value.spliterator(), false) + .map(item -> readItem(locationBuilder, item)) + .collect(collector()); } - private Object readItem(IndexedLocationBuilder locationBuilder, JsonValue itemValue) { + private Object readItem(IndexedLocationBuilder locationBuilder, JsonNode itemValue) { Location itemLocation = locationBuilder.nextLocation(); TypeInfo it = getItemType(); - if (itemValue.getValueType() == ValueType.NULL && it.isNonNull()) + if (itemValue.isNull() && it.isNonNull()) throw new InvalidResponseException("invalid null " + itemLocation); return JsonReader.readJson(itemLocation, it, itemValue, field); } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonBooleanReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonBooleanReader.java index b8352290e..1f296a47c 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonBooleanReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonBooleanReader.java @@ -1,23 +1,20 @@ package io.smallrye.graphql.client.impl.typesafe.json; -import static jakarta.json.JsonValue.ValueType.FALSE; -import static jakarta.json.JsonValue.ValueType.TRUE; - -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -class JsonBooleanReader extends Reader { - JsonBooleanReader(TypeInfo type, Location location, JsonValue value, FieldInfo field) { +class JsonBooleanReader extends Reader { + JsonBooleanReader(TypeInfo type, Location location, JsonNode value, FieldInfo field) { super(type, location, value, field); } @Override Object read() { - assert value.getValueType() == TRUE || value.getValueType() == FALSE; + assert value.isBoolean(); GraphQLClientValueHelper.check(location, value, boolean.class.equals(type.getRawType()) || Boolean.class.equals(type.getRawType())); - return value.getValueType() == TRUE; + return value.booleanValue(); } } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonMapReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonMapReader.java index ec4d98c7a..2154ab1af 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonMapReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonMapReader.java @@ -3,14 +3,14 @@ import java.util.HashMap; import java.util.Map; -import jakarta.json.JsonArray; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import io.smallrye.graphql.client.InvalidResponseException; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -class JsonMapReader extends Reader { +class JsonMapReader extends Reader { // these two strings denote the expected keys in the JSON representation of a map entry // for example, a single map entry can be serialized as `{"key":2,"value":"b"}` where the `key` and `value` @@ -23,7 +23,7 @@ class JsonMapReader extends Reader { // type of values expected to be present in the map private final TypeInfo valueType; - JsonMapReader(TypeInfo type, Location location, JsonArray value, FieldInfo field) { + JsonMapReader(TypeInfo type, Location location, ArrayNode value, FieldInfo field) { super(type, location, value, field); this.keyType = type.getKeyType(); this.valueType = type.getValueType(); @@ -34,21 +34,22 @@ Object read() { GraphQLClientValueHelper.check(location, value, type.isMap()); MapLocationBuilder locationBuilder = new MapLocationBuilder(location); Map result = new HashMap<>(); - for (JsonValue entry : value) { + for (JsonNode entry : value) { Location keyLocation = locationBuilder.nextKeyLocation(); Location valueLocation = locationBuilder.nextValueLocation(); - JsonValue keyJson = entry.asJsonObject().get(JSON_KEY_FOR_KEY); - if (keyJson.getValueType() == JsonValue.ValueType.NULL) { + JsonNode keyJson = entry.get(JSON_KEY_FOR_KEY); + if (keyJson == null || keyJson.isNull()) { throw new InvalidResponseException("unexpected null key at " + keyLocation); } - JsonValue valueJson = entry.asJsonObject().get(JSON_KEY_FOR_VALUE); - if (valueJson.getValueType() == JsonValue.ValueType.NULL && valueType.isNonNull()) { + JsonNode valueJson = entry.get(JSON_KEY_FOR_VALUE); + if ((valueJson == null || valueJson.isNull()) && valueType.isNonNull()) { throw new InvalidResponseException("unexpected null value at " + keyLocation); } Object keyDeserialized = JsonReader.readJson(keyLocation, keyType, keyJson, field); - Object valueDeserialized = JsonReader.readJson(valueLocation, valueType, valueJson, field); + Object valueDeserialized = JsonReader.readJson(valueLocation, valueType, + valueJson != null ? valueJson : com.fasterxml.jackson.databind.node.NullNode.getInstance(), field); result.put(keyDeserialized, valueDeserialized); } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNullReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNullReader.java index 422de06db..f6cfaaa5b 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNullReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNullReader.java @@ -2,13 +2,13 @@ import static io.smallrye.graphql.client.impl.typesafe.json.GraphQLClientValueHelper.check; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -class JsonNullReader extends Reader { - JsonNullReader(TypeInfo type, Location location, JsonValue value, FieldInfo field) { +class JsonNullReader extends Reader { + JsonNullReader(TypeInfo type, Location location, JsonNode value, FieldInfo field) { super(type, location, value, field); } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNumberReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNumberReader.java index 52c2a9ad2..4a19449fa 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNumberReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonNumberReader.java @@ -6,13 +6,13 @@ import java.util.OptionalInt; import java.util.OptionalLong; -import jakarta.json.JsonNumber; +import com.fasterxml.jackson.databind.JsonNode; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -class JsonNumberReader extends Reader { - JsonNumberReader(TypeInfo type, Location location, JsonNumber value, FieldInfo field) { +class JsonNumberReader extends Reader { + JsonNumberReader(TypeInfo type, Location location, JsonNode value, FieldInfo field) { super(type, location, value, field); } @@ -25,7 +25,7 @@ Object read() { } } - private Object read(Location location, JsonNumber value, Class rawType) { + private Object read(Location location, JsonNode value, Class rawType) { if (byte.class.equals(rawType) || Byte.class.equals(rawType)) return (byte) readIntBetween(location, value, Byte.MIN_VALUE, Byte.MAX_VALUE); if (char.class.equals(rawType) || Character.class.equals(rawType)) @@ -33,29 +33,43 @@ private Object read(Location location, JsonNumber value, Class rawType) { if (short.class.equals(rawType) || Short.class.equals(rawType)) return (short) readIntBetween(location, value, Short.MIN_VALUE, Short.MAX_VALUE); if (int.class.equals(rawType) || Integer.class.equals(rawType)) - return value.intValueExact(); + return readIntExact(location, value); if (long.class.equals(rawType) || Long.class.equals(rawType)) - return value.longValueExact(); + return readLongExact(location, value); if (float.class.equals(rawType) || Float.class.equals(rawType)) return (float) value.doubleValue(); if (double.class.equals(rawType) || Double.class.equals(rawType)) return value.doubleValue(); if (BigInteger.class.equals(rawType)) - return value.bigIntegerValueExact(); + return value.bigIntegerValue(); if (BigDecimal.class.equals(rawType) || Object.class.equals(rawType)) - return value.bigDecimalValue(); + return new BigDecimal(value.asText()); if (OptionalInt.class.equals(rawType)) - return OptionalInt.of(value.intValueExact()); + return OptionalInt.of(readIntExact(location, value)); if (OptionalLong.class.equals(rawType)) - return OptionalLong.of(value.longValueExact()); + return OptionalLong.of(readLongExact(location, value)); if (OptionalDouble.class.equals(rawType)) return OptionalDouble.of(value.doubleValue()); throw GraphQLClientValueHelper.fail(location, value); } - private int readIntBetween(Location location, JsonNumber value, int minValue, int maxValue) { - int intValue = value.intValueExact(); + private int readIntExact(Location location, JsonNode value) { + if (value.isFloatingPointNumber() || !value.canConvertToInt()) { + throw GraphQLClientValueHelper.fail(location, value); + } + return value.intValue(); + } + + private long readLongExact(Location location, JsonNode value) { + if (value.isFloatingPointNumber() || !value.canConvertToLong()) { + throw GraphQLClientValueHelper.fail(location, value); + } + return value.longValue(); + } + + private int readIntBetween(Location location, JsonNode value, int minValue, int maxValue) { + int intValue = readIntExact(location, value); GraphQLClientValueHelper.check(location, value, intValue >= minValue); GraphQLClientValueHelper.check(location, value, intValue <= maxValue); return intValue; diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonObjectReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonObjectReader.java index 75271bd4f..416fad35a 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonObjectReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonObjectReader.java @@ -6,16 +6,16 @@ import java.util.Map; -import jakarta.json.JsonObject; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.smallrye.graphql.client.InvalidResponseException; import io.smallrye.graphql.client.impl.SmallRyeGraphQLClientMessages; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -class JsonObjectReader extends Reader { - JsonObjectReader(TypeInfo type, Location location, JsonObject value, FieldInfo field) { +class JsonObjectReader extends Reader { + JsonObjectReader(TypeInfo type, Location location, ObjectNode value, FieldInfo field) { super(type, location, value, field); } @@ -29,7 +29,7 @@ Object read() { private Object readObject() { if (type.isUnion() || type.isInterface()) { - var subtype = type.subtype(value.getString("__typename")); + var subtype = type.subtype(value.get("__typename").asText()); var instance = subtype.newInstance(new Object[0]); subtype.fields().forEach(field -> { Object fieldValue = buildValue(location, value, field); @@ -61,10 +61,10 @@ private Object newInstance() { return newInstance(new Object[0]); } - private Object buildValue(Location location, JsonObject value, FieldInfo field) { + private Object buildValue(Location location, ObjectNode value, FieldInfo field) { String fieldName = field.getAlias().orElseGet(field::getName); Location fieldLocation = new Location(field.getType(), location.getDescription() + "." + fieldName); - JsonValue jsonFieldValue = value.get(fieldName); + JsonNode jsonFieldValue = value.get(fieldName); if (jsonFieldValue == null) { if (field.isNonNull()) throw new InvalidResponseException("missing " + fieldLocation); diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonReader.java index 2dc15dd11..4033c11f1 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonReader.java @@ -8,12 +8,11 @@ import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.OptionalLong; +import java.util.stream.StreamSupport; -import jakarta.json.JsonArray; -import jakarta.json.JsonNumber; -import jakarta.json.JsonObject; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.smallrye.graphql.client.GraphQLClientException; import io.smallrye.graphql.client.GraphQLError; @@ -24,24 +23,24 @@ import io.smallrye.graphql.client.typesafe.api.ErrorOr; import io.smallrye.graphql.client.typesafe.api.TypesafeResponse; -public class JsonReader extends Reader { - public static Object readJson(String description, TypeInfo type, JsonValue value, FieldInfo field) { +public class JsonReader extends Reader { + public static Object readJson(String description, TypeInfo type, JsonNode value, FieldInfo field) { return readJson(new Location(type, description), type, value, field); } - public static Object readJsonTypesafeResponse(String description, TypeInfo type, JsonValue value, FieldInfo field) { + public static Object readJsonTypesafeResponse(String description, TypeInfo type, JsonNode value, FieldInfo field) { return readJsonTypesafeResponse(new Location(type, description), type, value, field); } - static Object readJson(Location location, TypeInfo type, JsonValue value, FieldInfo field) { + static Object readJson(Location location, TypeInfo type, JsonNode value, FieldInfo field) { return new JsonReader(type, location, value, field).read(); } - static Object readJsonTypesafeResponse(Location location, TypeInfo type, JsonValue value, FieldInfo field) { + static Object readJsonTypesafeResponse(Location location, TypeInfo type, JsonNode value, FieldInfo field) { return new JsonReader(type, location, value, field).typesafeResponseRead(); } - private JsonReader(TypeInfo type, Location location, JsonValue value, FieldInfo field) { + private JsonReader(TypeInfo type, Location location, JsonNode value, FieldInfo field) { super(type, location, value, field); } @@ -85,12 +84,12 @@ private ErrorOr readErrorOr() { } private List readGraphQlClientErrors() { - return value.asJsonArray().stream() + return StreamSupport.stream(value.spliterator(), false) .map(ResponseReader::readError) .collect(toList()); } - private boolean isListOfErrors(JsonValue jsonValue) { + private boolean isListOfErrors(JsonNode jsonValue) { return isListOf(jsonValue, ErrorOr.class.getSimpleName()); } @@ -104,28 +103,30 @@ private GraphQLClientException cantApplyErrors(List errors) { } private Reader reader(Location location) { - switch (value.getValueType()) { + switch (value.getNodeType()) { case ARRAY: { if (type.isCollection()) { - return new JsonArrayReader(type, location, (JsonArray) value, field); + return new JsonArrayReader(type, location, (ArrayNode) value, field); } else if (type.isMap()) { - return new JsonMapReader(type, location, (JsonArray) value, field); + return new JsonMapReader(type, location, (ArrayNode) value, field); } else { throw new InvalidResponseException( "invalid " + type.getTypeName() + " value for " + location.getDescription() + ": " + value); } } case OBJECT: - return new JsonObjectReader(type, location, (JsonObject) value, field); + return new JsonObjectReader(type, location, (ObjectNode) value, field); case STRING: - return new JsonStringReader(type, location, (JsonString) value, field); + return new JsonStringReader(type, location, value, field); case NUMBER: - return new JsonNumberReader(type, location, (JsonNumber) value, field); - case TRUE: - case FALSE: + return new JsonNumberReader(type, location, value, field); + case BOOLEAN: return new JsonBooleanReader(type, location, value, field); case NULL: + case MISSING: return new JsonNullReader(type, location, value, field); + default: + break; } throw new InvalidResponseException("unexpected value type for " + location.getDescription() + ": " + value); } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonStringReader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonStringReader.java index e06e3a487..cc7f24ecf 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonStringReader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonStringReader.java @@ -8,55 +8,57 @@ import java.util.Date; import java.util.Locale; -import jakarta.json.JsonString; import jakarta.json.bind.annotation.JsonbDateFormat; import jakarta.json.bind.annotation.JsonbNumberFormat; import org.eclipse.microprofile.graphql.DateFormat; import org.eclipse.microprofile.graphql.NumberFormat; +import com.fasterxml.jackson.databind.JsonNode; + import io.smallrye.graphql.client.InvalidResponseException; import io.smallrye.graphql.client.impl.typesafe.reflection.ConstructionInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -class JsonStringReader extends Reader { - JsonStringReader(TypeInfo type, Location location, JsonString value, FieldInfo field) { +class JsonStringReader extends Reader { + JsonStringReader(TypeInfo type, Location location, JsonNode value, FieldInfo field) { super(type, location, value, field); } @Override Object read() { + String text = value.asText(); if (char.class.equals(type.getRawType()) || Character.class.equals(type.getRawType())) { - if (value.getChars().length() != 1) + if (text.length() != 1) throw GraphQLClientValueHelper.fail(location, value); - return value.getChars().charAt(0); + return text.charAt(0); } if (String.class.equals(type.getRawType()) || Object.class.equals(type.getRawType())) // TODO CharSequence - return value.getString(); + return text; if (type.isEnum()) return enumValue(); if (java.util.Date.class.equals(this.type.getRawType())) { - return formattedDate(value.getString()); + return formattedDate(text); } if (java.util.UUID.class.equals(this.type.getRawType())) - return java.util.UUID.fromString(value.getString()); + return java.util.UUID.fromString(text); if (java.util.Calendar.class.isAssignableFrom(this.type.getRawType())) { - return formattedCalendar(value.getString()); + return formattedCalendar(text); } if (Number.class.isAssignableFrom(this.type.getRawType()) && field != null && ((field.getAnnotation(NumberFormat.class) != null) || field.getAnnotation(JsonbNumberFormat.class) != null)) { - return formattedNumber(value.getString()); + return formattedNumber(text); } ConstructionInfo constructor = type.scalarConstructor() .orElseThrow(() -> GraphQLClientValueHelper.fail(location, value)); try { - return constructor.execute(value.getString()); + return constructor.execute(text); } catch (Exception e) { throw new RuntimeException("can't create scalar " + location, e); } @@ -181,6 +183,6 @@ private Number formattedNumber(String input) { @SuppressWarnings({ "rawtypes", "unchecked" }) private Enum enumValue() { - return Enum.valueOf((Class) type.getRawType(), value.getString()); + return Enum.valueOf((Class) type.getRawType(), value.asText()); } } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonUtils.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonUtils.java index 732155913..1c7d039b8 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonUtils.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/JsonUtils.java @@ -1,57 +1,56 @@ package io.smallrye.graphql.client.impl.typesafe.json; -import static jakarta.json.JsonValue.ValueType.ARRAY; -import static jakarta.json.JsonValue.ValueType.OBJECT; - +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.stream.Collectors; -import jakarta.json.JsonArray; -import jakarta.json.JsonNumber; -import jakarta.json.JsonObject; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; public class JsonUtils { - public static Object toValue(JsonValue value) { - switch (value.getValueType()) { + public static Object toValue(JsonNode value) { + switch (value.getNodeType()) { case NULL: return null; - case TRUE: - return true; - case FALSE: - return false; + case BOOLEAN: + return value.booleanValue(); case STRING: - return ((JsonString) value).getString(); + return value.asText(); case NUMBER: - return ((JsonNumber) value).numberValue(); + return value.numberValue(); case ARRAY: - return toList(value.asJsonArray()); + return toList((ArrayNode) value); case OBJECT: - return toMap(value.asJsonObject()); + return toMap((ObjectNode) value); + default: + break; } throw new UnsupportedOperationException(); } - public static Object toList(JsonArray value) { - return value.stream().map(JsonUtils::toValue).collect(Collectors.toList()); + public static List toList(ArrayNode value) { + List result = new ArrayList<>(); + for (JsonNode node : value) { + result.add(toValue(node)); + } + return result; } - public static Map toMap(JsonObject jsonObject) { + public static Map toMap(ObjectNode jsonObject) { if (jsonObject == null) return null; Map map = new LinkedHashMap<>(); - for (Entry entry : jsonObject.entrySet()) - map.put(entry.getKey(), toValue(entry.getValue())); + jsonObject.fields().forEachRemaining(entry -> map.put(entry.getKey(), toValue(entry.getValue()))); return map; } - public static boolean isListOf(JsonValue jsonValue, String typename) { - return jsonValue.getValueType() == ARRAY && - jsonValue.asJsonArray().size() > 0 && - jsonValue.asJsonArray().get(0).getValueType() == OBJECT && - typename.equals(jsonValue.asJsonArray().get(0).asJsonObject().getString("__typename", null)); + public static boolean isListOf(JsonNode jsonValue, String typename) { + return jsonValue.isArray() + && jsonValue.size() > 0 + && jsonValue.get(0).isObject() + && typename.equals( + jsonValue.get(0).has("__typename") ? jsonValue.get(0).get("__typename").asText(null) : null); } } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/Reader.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/Reader.java index 36c34a8d8..c939a9d7c 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/Reader.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/json/Reader.java @@ -2,12 +2,12 @@ import static java.util.Objects.requireNonNull; -import jakarta.json.JsonValue; +import com.fasterxml.jackson.databind.JsonNode; import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; -abstract class Reader { +abstract class Reader { protected final TypeInfo type; protected final Location location; protected final T value; diff --git a/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java b/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java index d8c5afe96..92f2a3588 100644 --- a/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java +++ b/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.impl.RequestImpl; /** @@ -102,11 +104,11 @@ public void testBuilderWithExtensions() { RequestImpl request = RequestImpl.builder("{ hello }") .extensions(extensions) .build(); - jakarta.json.JsonObject obj = request.toJsonObject(); - assertEquals("{ hello }", obj.getString("query")); - jakarta.json.JsonObject ext = obj.getJsonObject("extensions"); - assertEquals("abc-123", ext.getString("traceId")); - assertEquals(1, ext.getInt("priority")); + ObjectNode obj = request.toJsonObject(); + assertEquals("{ hello }", obj.get("query").asText()); + ObjectNode ext = (ObjectNode) obj.get("extensions"); + assertEquals("abc-123", ext.get("traceId").asText()); + assertEquals(1, ext.get("priority").intValue()); } @Test @@ -118,18 +120,18 @@ public void testBuilderWithAllFields() { .operationName("GetItem") .extensions(extensions) .build(); - jakarta.json.JsonObject obj = request.toJsonObject(); - assertEquals("query GetItem($id: Int) { item(id: $id) }", obj.getString("query")); - assertEquals("GetItem", obj.getString("operationName")); - assertEquals(42, obj.getJsonObject("variables").getInt("id")); - assertEquals("secret", obj.getJsonObject("extensions").getString("token")); + ObjectNode obj = request.toJsonObject(); + assertEquals("query GetItem($id: Int) { item(id: $id) }", obj.get("query").asText()); + assertEquals("GetItem", obj.get("operationName").asText()); + assertEquals(42, obj.get("variables").get("id").intValue()); + assertEquals("secret", obj.get("extensions").get("token").asText()); } @Test public void testBuilderNoExtensionsOmitsField() { RequestImpl request = RequestImpl.builder("{ hello }").build(); - jakarta.json.JsonObject obj = request.toJsonObject(); - assertFalse(obj.containsKey("extensions")); + ObjectNode obj = request.toJsonObject(); + assertFalse(obj.has("extensions")); } @Test @@ -137,7 +139,7 @@ public void testBuilderEmptyExtensionsOmitsField() { RequestImpl request = RequestImpl.builder("{ hello }") .extensions(new HashMap<>()) .build(); - jakarta.json.JsonObject obj = request.toJsonObject(); - assertFalse(obj.containsKey("extensions")); + ObjectNode obj = request.toJsonObject(); + assertFalse(obj.has("extensions")); } } diff --git a/client/tck/pom.xml b/client/tck/pom.xml index e4e54513d..ecea52857 100644 --- a/client/tck/pom.xml +++ b/client/tck/pom.xml @@ -40,8 +40,8 @@ jakarta.enterprise.cdi-api - jakarta.json - jakarta.json-api + com.fasterxml.jackson.core + jackson-databind jakarta.json.bind @@ -87,10 +87,5 @@ io.smallrye smallrye-graphql-client - - org.eclipse - yasson - compile - diff --git a/client/tck/src/main/java/tck/graphql/typesafe/TypesafeResponseBehavior.java b/client/tck/src/main/java/tck/graphql/typesafe/TypesafeResponseBehavior.java index 71ebacb3b..6dbae320f 100644 --- a/client/tck/src/main/java/tck/graphql/typesafe/TypesafeResponseBehavior.java +++ b/client/tck/src/main/java/tck/graphql/typesafe/TypesafeResponseBehavior.java @@ -7,10 +7,11 @@ import java.util.Map; import java.util.NoSuchElementException; -import jakarta.json.Json; - import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.GraphQLError; import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; import io.smallrye.graphql.client.typesafe.api.TypesafeResponse; @@ -34,7 +35,7 @@ static class Greeting { } @Test - void shouldParseExtensionsInTypesafeResponse() { + void shouldParseExtensionsInTypesafeResponse() throws Exception { fixture .returns("{" + "\"data\":{\"greetings\":\"something\"}," + @@ -51,10 +52,10 @@ void shouldParseExtensionsInTypesafeResponse() { TypesafeResponse result = api.greetings(); then(fixture.query()).isEqualTo("query greetings { greetings }"); - then(result.getExtensions()).isEqualTo(Json.createObjectBuilder() - .add("pi", 3.14159) - .add("extension", "bell") - .build()); + ObjectMapper mapper = new ObjectMapper() + .enable(com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + ObjectNode expectedExtensions = (ObjectNode) mapper.readTree("{\"pi\":3.14159,\"extension\":\"bell\"}"); + then(result.getExtensions()).isEqualTo(expectedExtensions); then(result.getTransportMeta()).isEqualTo( Map.of( "Accept", List.of("application/json;charset=utf-8"), diff --git a/common/jackson-jsonb-compat/pom.xml b/common/jackson-jsonb-compat/pom.xml new file mode 100644 index 000000000..983307d39 --- /dev/null +++ b/common/jackson-jsonb-compat/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + io.smallrye + smallrye-graphql-common-parent + 3.0.0.Beta5-SNAPSHOT + + + smallrye-graphql-jackson-jsonb-compat + SmallRye: GraphQL Jackson JSON-B Compatibility + Jackson module that honors JSON-B annotations for backward compatibility + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + jakarta.json.bind + jakarta.json.bind-api + + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + diff --git a/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbAnnotationIntrospector.java b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbAnnotationIntrospector.java new file mode 100644 index 000000000..1de82ff03 --- /dev/null +++ b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbAnnotationIntrospector.java @@ -0,0 +1,154 @@ +package io.smallrye.graphql.jackson.jsonb; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbDateFormat; +import jakarta.json.bind.annotation.JsonbNillable; +import jakarta.json.bind.annotation.JsonbNumberFormat; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbSubtype; +import jakarta.json.bind.annotation.JsonbTransient; +import jakarta.json.bind.annotation.JsonbTypeInfo; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder; + +public class JsonbAnnotationIntrospector extends NopAnnotationIntrospector { + + private static final long serialVersionUID = 1L; + + @Override + public PropertyName findNameForSerialization(Annotated a) { + JsonbProperty prop = a.getAnnotation(JsonbProperty.class); + if (prop != null && !prop.value().isEmpty()) { + return PropertyName.construct(prop.value()); + } + return null; + } + + @Override + public PropertyName findNameForDeserialization(Annotated a) { + JsonbProperty prop = a.getAnnotation(JsonbProperty.class); + if (prop != null && !prop.value().isEmpty()) { + return PropertyName.construct(prop.value()); + } + return null; + } + + @Override + public boolean hasIgnoreMarker(AnnotatedMember m) { + return m.hasAnnotation(JsonbTransient.class); + } + + @Override + public JsonInclude.Value findPropertyInclusion(Annotated a) { + // Field-level: @JsonbProperty(nillable = true) + JsonbProperty prop = a.getAnnotation(JsonbProperty.class); + if (prop != null && prop.nillable()) { + return JsonInclude.Value.construct(JsonInclude.Include.ALWAYS, JsonInclude.Include.ALWAYS); + } + // Class-level: @JsonbNillable + if (a.hasAnnotation(JsonbNillable.class)) { + return JsonInclude.Value.construct(JsonInclude.Include.ALWAYS, JsonInclude.Include.ALWAYS); + } + // Also check declaring class for field-level elements + if (a instanceof AnnotatedMember) { + Class declaring = ((AnnotatedMember) a).getDeclaringClass(); + if (declaring.isAnnotationPresent(JsonbNillable.class)) { + return JsonInclude.Value.construct(JsonInclude.Include.ALWAYS, JsonInclude.Include.ALWAYS); + } + } + return null; + } + + @Override + public Object findSerializer(Annotated a) { + JsonbDateFormat dateFormat = a.getAnnotation(JsonbDateFormat.class); + if (dateFormat != null) { + return new JsonbDateFormatSerializer(dateFormat.value(), dateFormat.locale()); + } + JsonbNumberFormat numberFormat = a.getAnnotation(JsonbNumberFormat.class); + if (numberFormat != null) { + return new JsonbNumberFormatSerializer(numberFormat.value(), numberFormat.locale()); + } + return null; + } + + @Override + public Object findDeserializer(Annotated a) { + JsonbDateFormat dateFormat = a.getAnnotation(JsonbDateFormat.class); + if (dateFormat != null) { + Class rawType = resolveRawType(a); + return new JsonbDateFormatDeserializer(dateFormat.value(), dateFormat.locale(), rawType); + } + JsonbNumberFormat numberFormat = a.getAnnotation(JsonbNumberFormat.class); + if (numberFormat != null) { + Class rawType = resolveRawType(a); + return new JsonbNumberFormatDeserializer(numberFormat.value(), numberFormat.locale(), rawType); + } + return null; + } + + private Class resolveRawType(Annotated a) { + if (a instanceof AnnotatedField) { + return ((AnnotatedField) a).getRawType(); + } + if (a instanceof AnnotatedMethod) { + AnnotatedMethod method = (AnnotatedMethod) a; + if (method.getParameterCount() > 0) { + return method.getRawParameterType(0); + } + return method.getRawReturnType(); + } + return Object.class; + } + + @Override + public TypeResolverBuilder findTypeResolver(MapperConfig config, AnnotatedClass ac, JavaType baseType) { + JsonbTypeInfo typeInfo = ac.getAnnotation(JsonbTypeInfo.class); + if (typeInfo != null) { + StdTypeResolverBuilder builder = new StdTypeResolverBuilder(); + builder.init(JsonTypeInfo.Id.NAME, null); + builder.inclusion(JsonTypeInfo.As.PROPERTY); + builder.typeProperty(typeInfo.key()); + return builder; + } + return null; + } + + @Override + public List findSubtypes(Annotated a) { + JsonbTypeInfo typeInfo = a.getAnnotation(JsonbTypeInfo.class); + if (typeInfo != null) { + List subtypes = new ArrayList<>(); + for (JsonbSubtype subtype : typeInfo.value()) { + subtypes.add(new NamedType(subtype.type(), subtype.alias())); + } + return subtypes; + } + return null; + } + + @Override + public JsonCreator.Mode findCreatorAnnotation(MapperConfig config, Annotated a) { + if (a.hasAnnotation(JsonbCreator.class)) { + return JsonCreator.Mode.PROPERTIES; + } + return null; + } +} diff --git a/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbCompatModule.java b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbCompatModule.java new file mode 100644 index 000000000..08cf44ee2 --- /dev/null +++ b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbCompatModule.java @@ -0,0 +1,18 @@ +package io.smallrye.graphql.jackson.jsonb; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class JsonbCompatModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public JsonbCompatModule() { + super("JsonbCompatModule"); + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + context.insertAnnotationIntrospector(new JsonbAnnotationIntrospector()); + } +} diff --git a/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbDateFormatDeserializer.java b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbDateFormatDeserializer.java new file mode 100644 index 000000000..dddb80eef --- /dev/null +++ b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbDateFormatDeserializer.java @@ -0,0 +1,72 @@ +package io.smallrye.graphql.jackson.jsonb; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +import jakarta.json.bind.annotation.JsonbDateFormat; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; + +public class JsonbDateFormatDeserializer extends JsonDeserializer implements ContextualDeserializer { + + private DateTimeFormatter formatter; + private Class targetType; + + public JsonbDateFormatDeserializer() { + // Default constructor required by Jackson + } + + public JsonbDateFormatDeserializer(String pattern, String locale, Class targetType) { + Locale loc = (locale == null || locale.isEmpty()) ? Locale.getDefault() : Locale.forLanguageTag(locale); + this.formatter = DateTimeFormatter.ofPattern(pattern, loc); + this.targetType = targetType; + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + if (property != null) { + JsonbDateFormat ann = property.getAnnotation(JsonbDateFormat.class); + if (ann != null) { + Class rawType = property.getType().getRawClass(); + return new JsonbDateFormatDeserializer(ann.value(), ann.locale(), rawType); + } + } + return this; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text = p.getText(); + TemporalAccessor parsed = formatter.parse(text); + + if (targetType == LocalDate.class) { + return LocalDate.from(parsed); + } else if (targetType == LocalDateTime.class) { + return LocalDateTime.from(parsed); + } else if (targetType == LocalTime.class) { + return LocalTime.from(parsed); + } else if (targetType == OffsetDateTime.class) { + return OffsetDateTime.from(parsed); + } else if (targetType == ZonedDateTime.class) { + return ZonedDateTime.from(parsed); + } else if (targetType == Instant.class) { + return Instant.from(parsed); + } else if (targetType == OffsetTime.class) { + return OffsetTime.from(parsed); + } + return parsed; + } +} diff --git a/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbDateFormatSerializer.java b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbDateFormatSerializer.java new file mode 100644 index 000000000..e0399cc0d --- /dev/null +++ b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbDateFormatSerializer.java @@ -0,0 +1,25 @@ +package io.smallrye.graphql.jackson.jsonb; + +import java.io.IOException; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class JsonbDateFormatSerializer extends JsonSerializer { + + private final DateTimeFormatter formatter; + + public JsonbDateFormatSerializer(String pattern, String locale) { + Locale loc = (locale == null || locale.isEmpty()) ? Locale.getDefault() : Locale.forLanguageTag(locale); + this.formatter = DateTimeFormatter.ofPattern(pattern, loc); + } + + @Override + public void serialize(TemporalAccessor value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(formatter.format(value)); + } +} diff --git a/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbNumberFormatDeserializer.java b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbNumberFormatDeserializer.java new file mode 100644 index 000000000..2af8ec598 --- /dev/null +++ b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbNumberFormatDeserializer.java @@ -0,0 +1,84 @@ +package io.smallrye.graphql.jackson.jsonb; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParseException; +import java.util.Locale; + +import jakarta.json.bind.annotation.JsonbNumberFormat; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; + +public class JsonbNumberFormatDeserializer extends JsonDeserializer implements ContextualDeserializer { + + private String pattern; + private String locale; + private Class targetType; + + public JsonbNumberFormatDeserializer() { + // Default constructor required by Jackson + } + + public JsonbNumberFormatDeserializer(String pattern, String locale, Class targetType) { + this.pattern = pattern; + this.locale = locale; + this.targetType = targetType; + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + if (property != null) { + JsonbNumberFormat ann = property.getAnnotation(JsonbNumberFormat.class); + if (ann != null) { + Class rawType = property.getType().getRawClass(); + return new JsonbNumberFormatDeserializer(ann.value(), ann.locale(), rawType); + } + } + return this; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text = p.getText(); + Locale loc = resolveLocale(ctxt); + DecimalFormat fmt = new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(loc)); + fmt.setParseBigDecimal(true); + + Number parsed; + try { + parsed = fmt.parse(text); + } catch (ParseException e) { + throw new IOException("Failed to parse number from '" + text + "' with pattern '" + pattern + "'", e); + } + + if (targetType == int.class || targetType == Integer.class) { + return parsed.intValue(); + } else if (targetType == long.class || targetType == Long.class) { + return parsed.longValue(); + } else if (targetType == double.class || targetType == Double.class) { + return parsed.doubleValue(); + } else if (targetType == float.class || targetType == Float.class) { + return parsed.floatValue(); + } else if (targetType == BigDecimal.class) { + return parsed instanceof BigDecimal ? parsed : new BigDecimal(parsed.toString()); + } else if (targetType == BigInteger.class) { + return parsed instanceof BigDecimal ? ((BigDecimal) parsed).toBigInteger() : BigInteger.valueOf(parsed.longValue()); + } + return parsed; + } + + private Locale resolveLocale(DeserializationContext ctxt) { + if (locale != null && !locale.isEmpty() + && !JsonbNumberFormat.DEFAULT_LOCALE.equals(locale)) { + return Locale.forLanguageTag(locale); + } + return ctxt.getConfig().getLocale(); + } +} diff --git a/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbNumberFormatSerializer.java b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbNumberFormatSerializer.java new file mode 100644 index 000000000..f0df775d2 --- /dev/null +++ b/common/jackson-jsonb-compat/src/main/java/io/smallrye/graphql/jackson/jsonb/JsonbNumberFormatSerializer.java @@ -0,0 +1,36 @@ +package io.smallrye.graphql.jackson.jsonb; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class JsonbNumberFormatSerializer extends JsonSerializer { + + private final String pattern; + private final String locale; + + public JsonbNumberFormatSerializer(String pattern, String locale) { + this.pattern = pattern; + this.locale = locale; + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + Locale loc = resolveLocale(serializers); + DecimalFormat fmt = new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(loc)); + gen.writeString(fmt.format(value)); + } + + private Locale resolveLocale(SerializerProvider serializers) { + if (locale != null && !locale.isEmpty() + && !jakarta.json.bind.annotation.JsonbNumberFormat.DEFAULT_LOCALE.equals(locale)) { + return Locale.forLanguageTag(locale); + } + return serializers.getConfig().getLocale(); + } +} diff --git a/common/jackson-jsonb-compat/src/test/java/io/smallrye/graphql/jackson/jsonb/JsonbAnnotationIntrospectorTest.java b/common/jackson-jsonb-compat/src/test/java/io/smallrye/graphql/jackson/jsonb/JsonbAnnotationIntrospectorTest.java new file mode 100644 index 000000000..59f4582f1 --- /dev/null +++ b/common/jackson-jsonb-compat/src/test/java/io/smallrye/graphql/jackson/jsonb/JsonbAnnotationIntrospectorTest.java @@ -0,0 +1,185 @@ +package io.smallrye.graphql.jackson.jsonb; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.util.Locale; + +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbDateFormat; +import jakarta.json.bind.annotation.JsonbNillable; +import jakarta.json.bind.annotation.JsonbNumberFormat; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbSubtype; +import jakarta.json.bind.annotation.JsonbTransient; +import jakarta.json.bind.annotation.JsonbTypeInfo; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +class JsonbAnnotationIntrospectorTest { + + private final ObjectMapper mapper = new ObjectMapper() + .registerModule(new JsonbCompatModule()) + .registerModule(new JavaTimeModule()); + + // -- @JsonbProperty -- + + static class PropertyBean { + @JsonbProperty("custom_name") + public String originalName = "value"; + } + + @Test + void jsonbPropertyRenamesFieldOnSerialization() throws Exception { + String json = mapper.writeValueAsString(new PropertyBean()); + assertThat(json).contains("\"custom_name\"").doesNotContain("\"originalName\""); + } + + @Test + void jsonbPropertyRenamesFieldOnDeserialization() throws Exception { + PropertyBean deserialized = mapper.readValue("{\"custom_name\":\"hello\"}", PropertyBean.class); + assertThat(deserialized.originalName).isEqualTo("hello"); + } + + // -- @JsonbTransient -- + + static class TransientBean { + public String visible = "yes"; + @JsonbTransient + public String hidden = "no"; + } + + @Test + void jsonbTransientExcludesField() throws Exception { + String json = mapper.writeValueAsString(new TransientBean()); + assertThat(json).contains("\"visible\"").doesNotContain("\"hidden\""); + } + + // -- @JsonbCreator -- + + static class CreatorBean { + private final String name; + private final int age; + + @JsonbCreator + public CreatorBean( + @JsonbProperty("name") String name, + @JsonbProperty("age") int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + } + + @Test + void jsonbCreatorUsesAnnotatedConstructor() throws Exception { + CreatorBean bean = mapper.readValue("{\"name\":\"Alice\",\"age\":30}", CreatorBean.class); + assertThat(bean.getName()).isEqualTo("Alice"); + assertThat(bean.getAge()).isEqualTo(30); + } + + // -- @JsonbNillable -- + + @JsonbNillable + static class NillableBean { + public String name = null; + public String other = "value"; + } + + @Test + void jsonbNillableIncludesNullFields() throws Exception { + String json = mapper.writeValueAsString(new NillableBean()); + assertThat(json).contains("\"name\":null").contains("\"other\":\"value\""); + } + + // -- @JsonbDateFormat -- + + static class DateFormatBean { + @JsonbDateFormat("dd.MM.yyyy") + public LocalDate date = LocalDate.of(2026, 6, 15); + } + + @Test + void jsonbDateFormatSerializesWithPattern() throws Exception { + String json = mapper.writeValueAsString(new DateFormatBean()); + assertThat(json).contains("\"15.06.2026\""); + } + + @Test + void jsonbDateFormatDeserializesWithPattern() throws Exception { + DateFormatBean bean = mapper.readValue("{\"date\":\"15.06.2026\"}", DateFormatBean.class); + assertThat(bean.date).isEqualTo(LocalDate.of(2026, 6, 15)); + } + + // -- @JsonbNumberFormat -- + + static class NumberFormatBean { + @JsonbNumberFormat("#,##0.00") + public double price = 1234.5; + } + + @Test + void jsonbNumberFormatSerializesWithPattern() throws Exception { + ObjectMapper usMapper = new ObjectMapper() + .registerModule(new JsonbCompatModule()) + .setLocale(Locale.US); + String json = usMapper.writeValueAsString(new NumberFormatBean()); + assertThat(json).contains("\"1,234.50\""); + } + + // -- @JsonbTypeInfo / @JsonbSubtype -- + + @JsonbTypeInfo(key = "__typename", value = { + @JsonbSubtype(alias = "Dog", type = Dog.class), + @JsonbSubtype(alias = "Cat", type = Cat.class) + }) + interface Animal { + String getName(); + } + + static class Dog implements Animal { + public String name; + public String breed; + + public String getName() { + return name; + } + } + + static class Cat implements Animal { + public String name; + public int lives; + + public String getName() { + return name; + } + } + + @Test + void jsonbTypeInfoDeserializesPolymorphicType() throws Exception { + String json = "{\"__typename\":\"Dog\",\"name\":\"Rex\",\"breed\":\"Labrador\"}"; + Animal animal = mapper.readValue(json, Animal.class); + assertThat(animal).isInstanceOf(Dog.class); + assertThat(((Dog) animal).breed).isEqualTo("Labrador"); + } + + @Test + void jsonbTypeInfoSerializesPolymorphicType() throws Exception { + Dog dog = new Dog(); + dog.name = "Rex"; + dog.breed = "Labrador"; + String json = mapper.writeValueAsString(dog); + // Should include the type discriminator + assertThat(json).contains("\"name\":\"Rex\"").contains("\"breed\":\"Labrador\""); + } +} diff --git a/common/pom.xml b/common/pom.xml index 8f757307e..026f7d696 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -17,5 +17,6 @@ schema-model schema-builder + jackson-jsonb-compat \ No newline at end of file diff --git a/common/schema-builder/pom.xml b/common/schema-builder/pom.xml index a33d8980b..c1081c65a 100644 --- a/common/schema-builder/pom.xml +++ b/common/schema-builder/pom.xml @@ -56,8 +56,13 @@ junit-jupiter - org.eclipse - yasson + com.fasterxml.jackson.core + jackson-databind + test + + + jakarta.json.bind + jakarta.json.bind-api test diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java index 90a2861ef..96ea84b9b 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java @@ -641,6 +641,10 @@ private static Map getAnnotationsWithFilter(org.jbo public static final DotName JACKSON_PROPERTY = DotName.createSimple("com.fasterxml.jackson.annotation.JsonProperty"); public static final DotName JACKSON_CREATOR = DotName.createSimple("com.fasterxml.jackson.annotation.JsonCreator"); public static final DotName JACKSON_FORMAT = DotName.createSimple("com.fasterxml.jackson.annotation.JsonFormat"); + public static final DotName JACKSON_SERIALIZE = DotName + .createSimple("com.fasterxml.jackson.databind.annotation.JsonSerialize"); + public static final DotName JACKSON_DESERIALIZE = DotName + .createSimple("com.fasterxml.jackson.databind.annotation.JsonDeserialize"); // Bean Validation Annotations (SmallRye extra, not part of the spec) public static final DotName JAVAX_BEAN_VALIDATION_NOT_NULL = DotName.createSimple("javax.validation.constraints.NotNull"); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java index 03d6543b7..7a2df4311 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Optional; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ParameterizedType; @@ -85,7 +86,8 @@ public static Optional getAdaptWith(Direction direction, ReferenceCre adaptWith.setToReference(Scalars.getScalar(to.name().toString())); } else { Annotations annotationsAplicableToMe = annotations.removeAnnotations(Annotations.ADAPT_WITH, - Annotations.JAKARTA_JSONB_TYPE_ADAPTER, Annotations.JAVAX_JSONB_TYPE_ADAPTER); + Annotations.JAKARTA_JSONB_TYPE_ADAPTER, Annotations.JAVAX_JSONB_TYPE_ADAPTER, + Annotations.JACKSON_SERIALIZE); // Remove the adaption annotation, as this is the type being adapted to Reference toRef = referenceCreator.createReferenceForAdapter(to, @@ -132,7 +134,6 @@ private static AdapterType getAdapterType(Annotations annotations) { } } - // Also add support for JsonB if (annotations.containsOneOfTheseAnnotations(Annotations.JAKARTA_JSONB_TYPE_ADAPTER)) { AnnotationValue annotationValue = annotations.getAnnotationValue(Annotations.JAKARTA_JSONB_TYPE_ADAPTER); if (annotationValue != null) { @@ -152,7 +153,21 @@ private static AdapterType getAdapterType(Annotations annotations) { } } - // TODO: add support for Jackson ? + // Jackson @JsonSerialize(converter=...) + if (annotations.containsOneOfTheseAnnotations(Annotations.JACKSON_SERIALIZE)) { + AnnotationInstance serializeAnn = annotations.getOneOfTheseAnnotations(Annotations.JACKSON_SERIALIZE) + .orElse(null); + if (serializeAnn != null) { + AnnotationValue converterValue = serializeAnn.value("converter"); + if (converterValue != null) { + AdaptWith adaptWith = new AdaptWith( + "com.fasterxml.jackson.databind.util.StdConverter", + "convert", "convert"); + Type type = converterValue.asClass(); + return new AdapterType(type, adaptWith); + } + } + } } return null; } diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java index ba9acabd0..46b520450 100644 --- a/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java @@ -24,16 +24,16 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; - import org.jboss.jandex.Index; import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + import io.smallrye.graphql.index.app.SomeDirective; import io.smallrye.graphql.schema.SchemaBuilder; import io.smallrye.graphql.schema.SchemaBuilderException; @@ -52,7 +52,7 @@ * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class SchemaBuilderTest { - private static final Jsonb JSONB = JsonbBuilder.create(new JsonbConfig().withFormatting(true)); + private static final ObjectMapper MAPPER = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); @Test public void testSchemaModelCreation() { @@ -62,6 +62,14 @@ public void testSchemaModelCreation() { assertNotNull(schema); } + private static String toJson(Object obj) { + try { + return MAPPER.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + @Test public void testConcurrentSchemaBuilding() throws Exception { Indexer indexer = new Indexer(); @@ -91,7 +99,7 @@ public void testConcurrentSchemaBuilding() throws Exception { assertNotNull(heroSchema); assertNotNull(movieSchema); - String basicSchemaString = JSONB.toJson(basicSchema); + String basicSchemaString = toJson(basicSchema); assertTrue(basicSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.basic.api.BasicType")); assertTrue(basicSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.basic.api.BasicInput")); assertTrue(basicSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.basic.api.BasicInterface")); @@ -99,7 +107,7 @@ public void testConcurrentSchemaBuilding() throws Exception { assertFalse(basicSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.superhero")); assertFalse(basicSchemaString.contains("io.smallrye.graphql")); - String heroSchemaString = JSONB.toJson(heroSchema); + String heroSchemaString = toJson(heroSchema); assertTrue(heroSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.superhero.model.SuperHero")); assertTrue(heroSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.superhero.model.Sidekick")); assertTrue(heroSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.superhero.model.Team")); @@ -107,7 +115,7 @@ public void testConcurrentSchemaBuilding() throws Exception { assertFalse(heroSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.basic")); assertFalse(heroSchemaString.contains("io.smallrye.graphql")); - String movieSchemaString = JSONB.toJson(movieSchema); + String movieSchemaString = toJson(movieSchema); assertTrue(movieSchemaString.contains("io.smallrye.graphql.index.app.Movie")); assertTrue(movieSchemaString.contains("io.smallrye.graphql.index.app.Person")); assertFalse(movieSchemaString.contains("org.eclipse.microprofile.graphql.tck.apps.basic")); diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java index 1d3751cad..9b1a024cb 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Scalars.java @@ -123,6 +123,8 @@ public static void addObject() { public static void addJson() { populateScalar("jakarta.json.JsonValue", JSON, Object.class.getName()); populateScalar("jakarta.json.JsonObject", JSON, Object.class.getName()); + populateScalar("com.fasterxml.jackson.databind.JsonNode", JSON, Object.class.getName()); + populateScalar("com.fasterxml.jackson.databind.node.ObjectNode", JSON, Object.class.getName()); } static { diff --git a/docs/pom.xml b/docs/pom.xml index bdc6166ff..65ae1fbbc 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -55,8 +55,8 @@ jakarta.enterprise.cdi-api - jakarta.json - jakarta.json-api + com.fasterxml.jackson.core + jackson-databind diff --git a/docs/snippets/examples/dynamicclient/MyClientUsage.java b/docs/snippets/examples/dynamicclient/MyClientUsage.java index f0d731ca8..fcb9f1269 100644 --- a/docs/snippets/examples/dynamicclient/MyClientUsage.java +++ b/docs/snippets/examples/dynamicclient/MyClientUsage.java @@ -7,7 +7,7 @@ import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; import jakarta.inject.Inject; -import jakarta.json.JsonArray; +import com.fasterxml.jackson.databind.node.ArrayNode; import java.util.List; import java.util.concurrent.ExecutionException; @@ -31,7 +31,7 @@ public void execute() throws ExecutionException, InterruptedException { field("superPowers")))); Response response = client.executeSync(document); // <2> - JsonArray heroesArray = response.getData().getJsonArray("allHeroesIn"); // <3> + ArrayNode heroesArray = (ArrayNode) response.getData().get("allHeroesIn"); // <3> List heroes = response.getList(SuperHero.class, "allHeroesIn"); // <4> } } diff --git a/pom.xml b/pom.xml index 67d4a906c..c64988dd9 100644 --- a/pom.xml +++ b/pom.xml @@ -28,8 +28,9 @@ 2.3.0 3.0.0.beta2 - 2.1.3 + 3.0.1 3.0.4 + 2.1.3 3.1.1 3.0.0 6.1.0 @@ -37,7 +38,7 @@ 6.0.0 25.0 24.0 - 1.16.5 + 1.16.5 5.0.12 2.13.0 @@ -141,7 +142,7 @@ io.micrometer micrometer-core - ${verison.io.micrometer} + ${version.io.micrometer} @@ -150,12 +151,6 @@ ${version.eclipse.microprofile.context-propagation} - - org.eclipse - yasson - ${version.yasson} - - io.smallrye jandex @@ -246,6 +241,16 @@ ${version.jakarta.websocket} provided + + jakarta.json.bind + jakarta.json.bind-api + ${version.jakarta.json-b.api} + + + org.eclipse + yasson + ${version.yasson} + jakarta.json jakarta.json-api @@ -346,6 +351,11 @@ smallrye-graphql-client-model-builder ${project.version} + + ${project.groupId} + smallrye-graphql-jackson-jsonb-compat + ${project.version} + com.graphql-java diff --git a/server/implementation-cdi/pom.xml b/server/implementation-cdi/pom.xml index e8bd33e83..15a5ed097 100644 --- a/server/implementation-cdi/pom.xml +++ b/server/implementation-cdi/pom.xml @@ -71,11 +71,6 @@ junit-jupiter test - - org.eclipse - yasson - test - org.jboss.weld weld-junit5 @@ -105,6 +100,12 @@ smallrye-graphql-schema-builder test + + + org.eclipse + yasson + test + diff --git a/server/implementation-cdi/src/test/java/io/smallrye/graphql/execution/CdiExecutionTest.java b/server/implementation-cdi/src/test/java/io/smallrye/graphql/execution/CdiExecutionTest.java index 7011325b5..b7c05e303 100644 --- a/server/implementation-cdi/src/test/java/io/smallrye/graphql/execution/CdiExecutionTest.java +++ b/server/implementation-cdi/src/test/java/io/smallrye/graphql/execution/CdiExecutionTest.java @@ -3,14 +3,12 @@ import static io.smallrye.graphql.JsonProviderHolder.JSON_PROVIDER; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; -import jakarta.json.JsonArray; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; @@ -24,6 +22,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.cdi.producer.GraphQLProducer; import io.smallrye.graphql.schema.SchemaBuilder; import io.smallrye.graphql.schema.model.Schema; @@ -52,181 +53,203 @@ public void init() { @Test public void testBasicQuery() { - JsonObject data = executeAndGetData(GET_HERO); + ObjectNode data = executeAndGetData(GET_HERO); - JsonObject superHero = data.getJsonObject("superHero"); + ObjectNode superHero = (ObjectNode) data.get("superHero"); assertNotNull(superHero); - assertFalse(superHero.isNull("name"), "name should not be null"); - assertEquals("Iron Man", superHero.getString("name")); + assertTrue(superHero.has("name") && !superHero.get("name").isNull(), "name should not be null"); + assertEquals("Iron Man", superHero.get("name").asText()); - assertFalse(superHero.isNull("primaryLocation"), "primaryLocation should not be null"); - assertEquals("Los Angeles, CA", superHero.getString("primaryLocation")); + assertTrue(superHero.has("primaryLocation") && !superHero.get("primaryLocation").isNull(), + "primaryLocation should not be null"); + assertEquals("Los Angeles, CA", superHero.get("primaryLocation").asText()); - assertFalse(superHero.isNull("realName"), "realName should not be null"); - assertEquals("Tony Stark", superHero.getString("realName")); + assertTrue(superHero.has("realName") && !superHero.get("realName").isNull(), "realName should not be null"); + assertEquals("Tony Stark", superHero.get("realName").asText()); // To test @Source - assertFalse(superHero.isNull("currentLocation"), "currentLocation should not be null (@Source not working)"); - assertEquals("Wachovia", superHero.getString("currentLocation")); + assertTrue(superHero.has("currentLocation") && !superHero.get("currentLocation").isNull(), + "currentLocation should not be null (@Source not working)"); + assertEquals("Wachovia", superHero.get("currentLocation").asText()); // To test @Source with extra default parameter - assertFalse(superHero.isNull("secretToken"), + assertTrue(superHero.has("secretToken") && !superHero.get("secretToken").isNull(), "secretToken should not be null (@Source with extra parameter not working)"); assertTrue( - superHero.getJsonObject("secretToken").getString("value").startsWith("********-****-****-****-********")); + ((ObjectNode) superHero.get("secretToken")).get("value").asText() + .startsWith("********-****-****-****-********")); // To test Number formatting (on return object fields) - assertFalse(superHero.isNull("idNumber"), "idNumber should not be null"); - assertEquals("ID-12345678", superHero.getString("idNumber")); + assertTrue(superHero.has("idNumber") && !superHero.get("idNumber").isNull(), "idNumber should not be null"); + assertEquals("ID-12345678", superHero.get("idNumber").asText()); // To test Date formatting (on return object fields) - assertFalse(superHero.isNull("dateOfLastCheckin"), "Transformation on Date not working"); - assertEquals("09/09/2019", superHero.getString("dateOfLastCheckin")); + assertTrue(superHero.has("dateOfLastCheckin") && !superHero.get("dateOfLastCheckin").isNull(), + "Transformation on Date not working"); + assertEquals("09/09/2019", superHero.get("dateOfLastCheckin").asText()); // To test DateTime formatting (on return object fields) - assertFalse(superHero.isNull("timeOfLastBattle"), "Transformation on DateTime not working"); - assertEquals("08:30:01 06-09-2019", superHero.getString("timeOfLastBattle")); + assertTrue(superHero.has("timeOfLastBattle") && !superHero.get("timeOfLastBattle").isNull(), + "Transformation on DateTime not working"); + assertEquals("08:30:01 06-09-2019", superHero.get("timeOfLastBattle").asText()); // To test Time formatting (on return object fields) - assertFalse(superHero.isNull("patrolStartTime"), "Transformation on Time not working"); - assertEquals("08:00", superHero.getString("patrolStartTime")); + assertTrue(superHero.has("patrolStartTime") && !superHero.get("patrolStartTime").isNull(), + "Transformation on Time not working"); + assertEquals("08:00", superHero.get("patrolStartTime").asText()); } @Test public void testDateTransformationOnQuery() { - JsonObject data = executeAndGetData(TRANSFORMED_DATE); + ObjectNode data = executeAndGetData(TRANSFORMED_DATE); - assertFalse(data.isNull("transformedDate"), "transformedDate should not be null"); - String transformedDate = data.getString("transformedDate"); + assertTrue(data.has("transformedDate") && !data.get("transformedDate").isNull(), + "transformedDate should not be null"); + String transformedDate = data.get("transformedDate").asText(); assertEquals("16 Aug 2016", transformedDate, "Date transformation on Query not working"); } @Test public void testNumberTransformationOnMutation() { - JsonObject data = executeAndGetData(TRANSFORMED_NUMBER); + ObjectNode data = executeAndGetData(TRANSFORMED_NUMBER); - assertFalse(data.isNull("transformedNumber"), "transformedNumber should not be null"); - assertEquals("number 345", data.getString("transformedNumber"), "Number transformation on Mutation not working"); + assertTrue(data.has("transformedNumber") && !data.get("transformedNumber").isNull(), + "transformedNumber should not be null"); + assertEquals("number 345", data.get("transformedNumber").asText(), "Number transformation on Mutation not working"); } @Test public void testNumberTransformationOnArgument() { - JsonObject data = executeAndGetData(TRANSFORMED_ARGUMENT); + ObjectNode data = executeAndGetData(TRANSFORMED_ARGUMENT); - assertFalse(data.isNull("idNumberWithCorrectFormat"), "idNumberWithCorrectFormat should not be null"); - assertFalse(data.getJsonObject("idNumberWithCorrectFormat").isNull("idNumber"), "idNumber should not be null"); - assertEquals("ID-88888888", data.getJsonObject("idNumberWithCorrectFormat").getString("idNumber"), + assertTrue(data.has("idNumberWithCorrectFormat") && !data.get("idNumberWithCorrectFormat").isNull(), + "idNumberWithCorrectFormat should not be null"); + ObjectNode idNumberObj = (ObjectNode) data.get("idNumberWithCorrectFormat"); + assertTrue(idNumberObj.has("idNumber") && !idNumberObj.get("idNumber").isNull(), "idNumber should not be null"); + assertEquals("ID-88888888", idNumberObj.get("idNumber").asText(), "Number transformation on Argument not working"); } @Test public void testBasicMutation() { - JsonObject data = executeAndGetData(MUTATION_BASIC); + ObjectNode data = executeAndGetData(MUTATION_BASIC); - assertFalse(data.isNull("addHeroToTeam"), "addHeroToTeam should not be null"); + assertTrue(data.has("addHeroToTeam") && !data.get("addHeroToTeam").isNull(), "addHeroToTeam should not be null"); - assertFalse(data.getJsonObject("addHeroToTeam").isNull("name"), "name should not be null"); - assertEquals("Avengers", data.getJsonObject("addHeroToTeam").getString("name"), + ObjectNode addHeroToTeam = (ObjectNode) data.get("addHeroToTeam"); + assertTrue(addHeroToTeam.has("name") && !addHeroToTeam.get("name").isNull(), "name should not be null"); + assertEquals("Avengers", addHeroToTeam.get("name").asText(), "Wrong team name while adding member"); - assertFalse(data.getJsonObject("addHeroToTeam").isNull("members"), "members should not be null"); - assertEquals(4, data.getJsonObject("addHeroToTeam").getJsonArray("members").size(), + assertTrue(addHeroToTeam.has("members") && !addHeroToTeam.get("members").isNull(), "members should not be null"); + assertEquals(4, ((ArrayNode) addHeroToTeam.get("members")).size(), "Wrong team size while adding member"); } @Test public void testMutationWithObjectArgument() { - JsonObject data = executeAndGetData(MUTATION_COMPLEX); + ObjectNode data = executeAndGetData(MUTATION_COMPLEX); - assertFalse(data.isNull("createNewHero"), "createNewHero should not be null"); + assertTrue(data.has("createNewHero") && !data.get("createNewHero").isNull(), "createNewHero should not be null"); - assertFalse(data.getJsonObject("createNewHero").isNull("name"), "name should not be null"); - assertEquals("Captain America", data.getJsonObject("createNewHero").getString("name"), + ObjectNode createNewHero = (ObjectNode) data.get("createNewHero"); + assertTrue(createNewHero.has("name") && !createNewHero.get("name").isNull(), "name should not be null"); + assertEquals("Captain America", createNewHero.get("name").asText(), "Wrong name while adding hero"); - assertFalse(data.getJsonObject("createNewHero").isNull("superPowers"), "superPowers should not be null"); - assertEquals(2, data.getJsonObject("createNewHero").getJsonArray("superPowers").size(), + assertTrue(createNewHero.has("superPowers") && !createNewHero.get("superPowers").isNull(), + "superPowers should not be null"); + assertEquals(2, ((ArrayNode) createNewHero.get("superPowers")).size(), "Wrong size superPowers while adding member"); } @Test public void testMutationScalarJavaMapping() { - JsonObject data = executeAndGetData(MUTATION_SCALAR_MAPPING); + ObjectNode data = executeAndGetData(MUTATION_SCALAR_MAPPING); - assertFalse(data.isNull("provisionHero"), "provisionHero should not be null"); + assertTrue(data.has("provisionHero") && !data.get("provisionHero").isNull(), "provisionHero should not be null"); - assertFalse(data.getJsonObject("provisionHero").isNull("name"), "name should not be null"); - assertEquals("Starlord", data.getJsonObject("provisionHero").getString("name"), + ObjectNode provisionHero = (ObjectNode) data.get("provisionHero"); + assertTrue(provisionHero.has("name") && !provisionHero.get("name").isNull(), "name should not be null"); + assertEquals("Starlord", provisionHero.get("name").asText(), "Wrong name while provisioning hero"); - assertFalse(data.getJsonObject("provisionHero").isNull("equipment"), "equipment should not be null"); - assertEquals(1, data.getJsonObject("provisionHero").getJsonArray("equipment").size(), + assertTrue(provisionHero.has("equipment") && !provisionHero.get("equipment").isNull(), + "equipment should not be null"); + assertEquals(1, ((ArrayNode) provisionHero.get("equipment")).size(), "Wrong size equipment while provisioning member"); } @Test public void testMutationWithComplexDefault() { - JsonObject data = executeAndGetData(MUTATION_COMPLEX_DEFAULT); + ObjectNode data = executeAndGetData(MUTATION_COMPLEX_DEFAULT); - assertFalse(data.isNull("provisionHero"), "provisionHero should not be null"); + assertTrue(data.has("provisionHero") && !data.get("provisionHero").isNull(), "provisionHero should not be null"); - assertFalse(data.getJsonObject("provisionHero").isNull("name"), "name should not be null"); - assertEquals("Spider Man", data.getJsonObject("provisionHero").getString("name"), + ObjectNode provisionHero = (ObjectNode) data.get("provisionHero"); + assertTrue(provisionHero.has("name") && !provisionHero.get("name").isNull(), "name should not be null"); + assertEquals("Spider Man", provisionHero.get("name").asText(), "Wrong name while provisioning hero"); - assertFalse(data.getJsonObject("provisionHero").isNull("equipment"), "equipment should not be null"); - assertEquals(1, data.getJsonObject("provisionHero").getJsonArray("equipment").size(), + assertTrue(provisionHero.has("equipment") && !provisionHero.get("equipment").isNull(), + "equipment should not be null"); + assertEquals(1, ((ArrayNode) provisionHero.get("equipment")).size(), "Wrong size equipment while provisioning member"); } @Test public void testMutationWithArrayInput() { - JsonObject data = executeAndGetData(MUTATION_COMPLEX_ARRAY); + ObjectNode data = executeAndGetData(MUTATION_COMPLEX_ARRAY); - assertFalse(data.isNull("createNewHeroesWithArray"), "createNewHeroesWithArray should not be null"); + assertTrue(data.has("createNewHeroesWithArray") && !data.get("createNewHeroesWithArray").isNull(), + "createNewHeroesWithArray should not be null"); - assertEquals(1, data.getJsonArray("createNewHeroesWithArray").size(), + assertEquals(1, ((ArrayNode) data.get("createNewHeroesWithArray")).size(), "Wrong size array while createNewHeroesWithArray"); } @Test public void testMutationWithCollectionInput() { - JsonObject data = executeAndGetData(MUTATION_COMPLEX_COLLECTION); + ObjectNode data = executeAndGetData(MUTATION_COMPLEX_COLLECTION); - assertFalse(data.isNull("createNewHeroes"), "createNewHeroes should not be null"); + assertTrue(data.has("createNewHeroes") && !data.get("createNewHeroes").isNull(), + "createNewHeroes should not be null"); - assertEquals(1, data.getJsonArray("createNewHeroes").size(), + assertEquals(1, ((ArrayNode) data.get("createNewHeroes")).size(), "Wrong size array while createNewHeroes"); } @Test public void testMutationWithCollectionTransformationInput() { - JsonObject data = executeAndGetData(MUTATION_COMPLEX_TRANSFORMATION_COLLECTION); + ObjectNode data = executeAndGetData(MUTATION_COMPLEX_TRANSFORMATION_COLLECTION); - assertFalse(data.isNull("createNewHero"), "createNewHero should not be null"); + assertTrue(data.has("createNewHero") && !data.get("createNewHero").isNull(), "createNewHero should not be null"); - assertFalse(data.getJsonObject("createNewHero").isNull("name"), "name should not be null"); - assertEquals("Black Panther", data.getJsonObject("createNewHero").getString("name"), + ObjectNode createNewHero = (ObjectNode) data.get("createNewHero"); + assertTrue(createNewHero.has("name") && !createNewHero.get("name").isNull(), "name should not be null"); + assertEquals("Black Panther", createNewHero.get("name").asText(), "Wrong name while createNewHero hero"); - assertFalse(data.getJsonObject("createNewHero").isNull("agesOfKids"), "agesOfKids should not be null"); + assertTrue(createNewHero.has("agesOfKids") && !createNewHero.get("agesOfKids").isNull(), + "agesOfKids should not be null"); - JsonArray jsonArray = data.getJsonObject("createNewHero").getJsonArray("agesOfKids"); + ArrayNode jsonArray = (ArrayNode) createNewHero.get("agesOfKids"); assertEquals(2, jsonArray.size(), "Wrong size agesOfKids while createNewHero member"); - Object[] receivedKids = new Object[] { jsonArray.getJsonString(0).toString(), jsonArray.getJsonString(1).toString() }; + Object[] receivedKids = new Object[] { "\"" + jsonArray.get(0).asText() + "\"", + "\"" + jsonArray.get(1).asText() + "\"" }; Object[] expectedKids = new Object[] { "\"3 years\"", "\"5 years\"" }; assertArrayEquals(expectedKids, receivedKids); @@ -235,111 +258,119 @@ public void testMutationWithCollectionTransformationInput() { @Test public void testMutationWithScalarDateInput() { - JsonObject data = executeAndGetData(MUTATION_SCALAR_DATE_INPUT); + ObjectNode data = executeAndGetData(MUTATION_SCALAR_DATE_INPUT); - assertFalse(data.isNull("startPatrolling"), "startPatrolling should not be null"); + assertTrue(data.has("startPatrolling") && !data.get("startPatrolling").isNull(), + "startPatrolling should not be null"); - assertFalse(data.getJsonObject("startPatrolling").isNull("name"), "name should not be null"); - assertEquals("Starlord", data.getJsonObject("startPatrolling").getString("name"), + ObjectNode startPatrolling = (ObjectNode) data.get("startPatrolling"); + assertTrue(startPatrolling.has("name") && !startPatrolling.get("name").isNull(), "name should not be null"); + assertEquals("Starlord", startPatrolling.get("name").asText(), "Wrong name while startPatrolling"); - assertFalse(data.getJsonObject("startPatrolling").isNull("patrolStartTime"), + assertTrue(startPatrolling.has("patrolStartTime") && !startPatrolling.get("patrolStartTime").isNull(), "patrolStartTime should not be null"); - assertEquals("20:00", data.getJsonObject("startPatrolling").getString("patrolStartTime"), + assertEquals("20:00", startPatrolling.get("patrolStartTime").asText(), "Wrong time while patrolStartTime"); } @Test public void testMutationWithScalarNumberInput() { - JsonObject data = executeAndGetData(MUTATION_SCALAR_NUMBER_INPUT); + ObjectNode data = executeAndGetData(MUTATION_SCALAR_NUMBER_INPUT); - assertFalse(data.isNull("idNumber"), "idNumber should not be null"); + assertTrue(data.has("idNumber") && !data.get("idNumber").isNull(), "idNumber should not be null"); - assertFalse(data.getJsonObject("idNumber").isNull("name"), "name should not be null"); - assertEquals("Starlord", data.getJsonObject("idNumber").getString("name"), + ObjectNode idNumber = (ObjectNode) data.get("idNumber"); + assertTrue(idNumber.has("name") && !idNumber.get("name").isNull(), "name should not be null"); + assertEquals("Starlord", idNumber.get("name").asText(), "Wrong name while idNumber"); - assertFalse(data.getJsonObject("idNumber").isNull("idNumber"), + assertTrue(idNumber.has("idNumber") && !idNumber.get("idNumber").isNull(), "idNumber should not be null"); - assertEquals("ID-77777777", data.getJsonObject("idNumber").getString("idNumber"), + assertEquals("ID-77777777", idNumber.get("idNumber").asText(), "Wrong idNumber while idNumber"); } @Test public void testMutationWithInvalidTimeInput() { - JsonArray errors = executeAndGetError(MUTATION_INVALID_TIME_SCALAR); + ArrayNode errors = executeAndGetError(MUTATION_INVALID_TIME_SCALAR); assertEquals(1, errors.size(), "Wrong size for errors while startPatrolling with wrong date"); - JsonObject error = errors.getJsonObject(0); + ObjectNode error = (ObjectNode) errors.get(0); - assertFalse(error.isNull("message"), "message should not be null"); + assertTrue(error.has("message") && !error.get("message").isNull(), "message should not be null"); assertEquals( "argument 'time' with value 'StringValue{value='Today'}' is not a valid 'Time'", - error.getString("message"), + error.get("message").asText(), "Wrong error message while startPatrolling with wrong date"); } @Test public void testMutationWithInvalidNumberInput() { - JsonArray errors = executeAndGetError(MUTATION_INVALID_NUMBER_SCALAR); + ArrayNode errors = executeAndGetError(MUTATION_INVALID_NUMBER_SCALAR); assertEquals(1, errors.size(), "Wrong size for errors while updateItemPowerLevel with wrong number"); - JsonObject error = errors.getJsonObject(0); + ObjectNode error = (ObjectNode) errors.get(0); - assertFalse(error.isNull("message"), "message should not be null"); + assertTrue(error.has("message") && !error.get("message").isNull(), "message should not be null"); assertEquals( "Validation error (WrongType@[updateItemPowerLevel]) : argument 'powerLevel' with value 'StringValue{value='Unlimited'}' is not a valid 'Int' - SRGQL000022: Can not parse a number from [StringValue{value='Unlimited'}]", - error.getString("message"), + error.get("message").asText(), "Wrong error message while updateItemPowerLevel with wrong number"); } @Test public void testParsingInvalidNumberScalar() { - JsonArray errors = executeAndGetError(MUTATION_INVALID_INTEGER_SCALAR); + ArrayNode errors = executeAndGetError(MUTATION_INVALID_INTEGER_SCALAR); assertEquals(1, errors.size(), "Wrong size for errors while updateItemPowerLevel with wrong number"); - JsonObject error = errors.getJsonObject(0); + ObjectNode error = (ObjectNode) errors.get(0); - assertFalse(error.isNull("message"), "message should not be null"); + assertTrue(error.has("message") && !error.get("message").isNull(), "message should not be null"); assertEquals( "Validation error (WrongType@[updateItemPowerLevel]) : argument 'powerLevel' with value 'StringValue{value='3.14'}' is not a valid 'Int' - SRGQL000021: Can not parse a integer from [StringValue{value='3.14'}]", - error.getString("message"), + error.get("message").asText(), "Wrong error message while updateItemPowerLevel with wrong number"); } @Test public void testDefaultTimeScalarFormat() { - JsonObject data = executeAndGetData(QUERY_DEFAULT_TIME_FORMAT); + ObjectNode data = executeAndGetData(QUERY_DEFAULT_TIME_FORMAT); - assertFalse(data.isNull("testScalarsInPojo"), "testScalarsInPojo should not be null"); + assertTrue(data.has("testScalarsInPojo") && !data.get("testScalarsInPojo").isNull(), + "testScalarsInPojo should not be null"); - assertFalse(data.getJsonObject("testScalarsInPojo").isNull("timeObject"), "timeObject should not be null"); - assertEquals("11:46:34.263", data.getJsonObject("testScalarsInPojo").getString("timeObject"), + ObjectNode testScalarsInPojo = (ObjectNode) data.get("testScalarsInPojo"); + assertTrue(testScalarsInPojo.has("timeObject") && !testScalarsInPojo.get("timeObject").isNull(), + "timeObject should not be null"); + assertEquals("11:46:34.263", testScalarsInPojo.get("timeObject").asText(), "Wrong wrong time format"); } @Test public void testInputWithDifferentNameOnInputAndType() { - JsonObject data = executeAndGetData(MUTATION_NAME_DIFF_ON_INPUT_AND_TYPE); + ObjectNode data = executeAndGetData(MUTATION_NAME_DIFF_ON_INPUT_AND_TYPE); - assertFalse(data.isNull("createNewHero"), "createNewHero should not be null"); + assertTrue(data.has("createNewHero") && !data.get("createNewHero").isNull(), "createNewHero should not be null"); - assertFalse(data.getJsonObject("createNewHero").isNull("sizeOfTShirt"), "sizeOfTShirt should not be null"); - assertEquals("XL", data.getJsonObject("createNewHero").getString("sizeOfTShirt"), + ObjectNode createNewHero = (ObjectNode) data.get("createNewHero"); + assertTrue(createNewHero.has("sizeOfTShirt") && !createNewHero.get("sizeOfTShirt").isNull(), + "sizeOfTShirt should not be null"); + assertEquals("XL", createNewHero.get("sizeOfTShirt").asText(), "Wrong sizeOfTShirt "); } - private JsonObject executeAndGetData(String graphQL) { + private ObjectNode executeAndGetData(String graphQL) { JsonObjectResponseWriter jor = new JsonObjectResponseWriter(graphQL); ExecutionService executionService = LookupService.get().getInstance(ExecutionService.class).get(); executionService.executeSync(toJsonObject(graphQL), jor); @@ -349,10 +380,10 @@ private JsonObject executeAndGetData(String graphQL) { String prettyData = result.getExecutionResultAsString(); LOG.info(prettyData); - return result.getExecutionResultAsJsonObject().getJsonObject(DATA); + return (ObjectNode) result.getExecutionResultAsJsonObject().get(DATA); } - private JsonArray executeAndGetError(String graphQL) { + private ArrayNode executeAndGetError(String graphQL) { JsonObjectResponseWriter jor = new JsonObjectResponseWriter(graphQL); ExecutionService executionService = LookupService.get().getInstance(ExecutionService.class).get(); executionService.executeSync(toJsonObject(graphQL), jor); @@ -361,7 +392,7 @@ private JsonArray executeAndGetError(String graphQL) { String prettyData = result.getExecutionResultAsString(); LOG.info(prettyData); - return result.getExecutionResultAsJsonObject().getJsonArray(ERRORS); + return (ArrayNode) result.getExecutionResultAsJsonObject().get(ERRORS); } private JsonObject toJsonObject(String graphQL) { diff --git a/server/implementation-servlet/pom.xml b/server/implementation-servlet/pom.xml index adaa66fe5..32d2d73dd 100644 --- a/server/implementation-servlet/pom.xml +++ b/server/implementation-servlet/pom.xml @@ -57,16 +57,15 @@ jakarta.annotation-api provided - - jakarta.json.bind - jakarta.json.bind-api - provided - jakarta.json jakarta.json-api provided + + com.fasterxml.jackson.core + jackson-databind + org.eclipse.microprofile.config microprofile-config-api diff --git a/server/implementation-servlet/src/main/java/io/smallrye/graphql/servlet/HttpServletResponseWriter.java b/server/implementation-servlet/src/main/java/io/smallrye/graphql/servlet/HttpServletResponseWriter.java index b64a52218..b797ef979 100644 --- a/server/implementation-servlet/src/main/java/io/smallrye/graphql/servlet/HttpServletResponseWriter.java +++ b/server/implementation-servlet/src/main/java/io/smallrye/graphql/servlet/HttpServletResponseWriter.java @@ -1,13 +1,11 @@ package io.smallrye.graphql.servlet; -import static io.smallrye.graphql.JsonProviderHolder.JSON_PROVIDER; - import java.io.IOException; -import jakarta.json.JsonWriter; -import jakarta.json.JsonWriterFactory; import jakarta.servlet.http.HttpServletResponse; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.smallrye.graphql.execution.ExecutionResponse; import io.smallrye.graphql.execution.ExecutionResponseWriter; @@ -18,7 +16,7 @@ */ public class HttpServletResponseWriter implements ExecutionResponseWriter { private static final String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8"; - private static final JsonWriterFactory jsonWriterFactory = JSON_PROVIDER.createWriterFactory(null); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final HttpServletResponse response; @@ -29,9 +27,9 @@ public HttpServletResponseWriter(HttpServletResponse response) { @Override public void write(ExecutionResponse executionResponse) { if (executionResponse != null) { - try (JsonWriter jsonWriter = jsonWriterFactory.createWriter(response.getOutputStream())) { - response.setContentType(APPLICATION_JSON_UTF8); - jsonWriter.writeObject(executionResponse.getExecutionResultAsJsonObject()); + response.setContentType(APPLICATION_JSON_UTF8); + try { + OBJECT_MAPPER.writeValue(response.getOutputStream(), executionResponse.getExecutionResultAsJsonObject()); } catch (IOException ex) { throw new RuntimeException(ex); } diff --git a/server/implementation/pom.xml b/server/implementation/pom.xml index b379f6d4c..68f8dbb51 100644 --- a/server/implementation/pom.xml +++ b/server/implementation/pom.xml @@ -26,6 +26,24 @@ smallrye-graphql-schema-model + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + io.smallrye + smallrye-graphql-jackson-jsonb-compat + + jakarta.json.bind diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java index c428120b5..8850326ec 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java @@ -5,7 +5,6 @@ import static graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY; import static io.smallrye.graphql.SmallRyeGraphQLServerLogging.log; -import java.io.StringReader; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -21,13 +20,11 @@ import java.util.Stack; import java.util.stream.Collectors; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.bind.Jsonb; - import org.eclipse.microprofile.graphql.Name; import com.apollographql.federation.graphqljava.Federation; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.Scalars; import graphql.introspection.Introspection.DirectiveLocation; @@ -54,7 +51,6 @@ import graphql.schema.TypeResolver; import graphql.schema.visibility.BlockedFields; import graphql.schema.visibility.GraphqlFieldVisibility; -import io.smallrye.graphql.JsonProviderHolder; import io.smallrye.graphql.SmallRyeGraphQLServerMessages; import io.smallrye.graphql.execution.Classes; import io.smallrye.graphql.execution.datafetcher.BatchDataFetcher; @@ -66,7 +62,7 @@ import io.smallrye.graphql.execution.resolver.InterfaceResolver; import io.smallrye.graphql.execution.resolver.UnionOutputRegistry; import io.smallrye.graphql.execution.resolver.UnionResolver; -import io.smallrye.graphql.json.JsonBCreator; +import io.smallrye.graphql.json.JacksonCreator; import io.smallrye.graphql.json.JsonInputRegistry; import io.smallrye.graphql.scalar.GraphQLScalarTypes; import io.smallrye.graphql.scalar.custom.FloatCoercing; @@ -205,7 +201,7 @@ private void generateGraphQLSchema() { // Allow custom extension schemaBuilder = eventEmitter.fireBeforeSchemaBuild(schemaBuilder); - Map overrides = eventEmitter.fireOverrideJsonbConfig(); + Map overrides = eventEmitter.fireOverrideObjectMapperConfig(); JsonInputRegistry.override(overrides); if (Config.get().isFederationEnabled()) { @@ -1180,8 +1176,12 @@ private Object sanitizeDefaultValue(Field field) { } } - Jsonb jsonB = JsonBCreator.getJsonB(deserType.getName()); - return jsonB.fromJson(jsonString, deserType); + ObjectMapper objectMapper = JacksonCreator.getObjectMapper(deserType.getName()); + try { + return objectMapper.readValue(jsonString, deserType); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize default value: " + jsonString, e); + } } if (Classes.isNumberLikeType(field.getReference().getGraphQLClassName())) { @@ -1197,10 +1197,8 @@ private Object sanitizeDefaultValue(Field field) { private boolean isJsonString(String string) { if (string != null && !string.isEmpty() && (string.contains("{") || string.contains("["))) { - try (StringReader stringReader = new StringReader(string); - JsonReader jsonReader = jsonReaderFactory.createReader(stringReader)) { - - jsonReader.readValue(); + try { + JSON_OBJECT_MAPPER.readTree(string); return true; } catch (Exception ex) { // Not a valid json @@ -1250,7 +1248,7 @@ private Type getTypeForField(Field field) { private static final String COMMA = ","; - private static final JsonReaderFactory jsonReaderFactory = JsonProviderHolder.JSON_PROVIDER.createReaderFactory(null); + private static final ObjectMapper JSON_OBJECT_MAPPER = new ObjectMapper(); private static final String CONTEXT = "io.smallrye.graphql.api.Context"; private static final String OBSERVES = "javax.enterprise.event.Observes"; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java index 61971d622..1e8737f5d 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionResponse.java @@ -1,7 +1,5 @@ package io.smallrye.graphql.execution; -import static io.smallrye.graphql.JsonProviderHolder.JSON_PROVIDER; - import java.io.StringReader; import java.math.BigDecimal; import java.math.BigInteger; @@ -13,17 +11,20 @@ import java.util.Stack; import java.util.stream.Collectors; -import jakarta.json.JsonArray; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonBuilderFactory; import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.JsonValue; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; +import jakarta.json.spi.JsonProvider; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import graphql.ExecutionResult; import graphql.GraphQLError; @@ -37,11 +38,8 @@ */ public class ExecutionResponse { - private static final JsonBuilderFactory jsonObjectFactory = JSON_PROVIDER.createBuilderFactory(null); - private static final JsonReaderFactory jsonReaderFactory = JSON_PROVIDER.createReaderFactory(null); - private static final Jsonb jsonB = JsonbBuilder.create(new JsonbConfig() - .withNullValues(Boolean.TRUE) - .withFormatting(Boolean.TRUE)); + private static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); + private static final JsonNodeFactory NODE_FACTORY = JsonNodeFactory.instance; private static final ExecutionErrorsService errorsService = new ExecutionErrorsService(); private final ExecutionResult executionResult; @@ -72,155 +70,167 @@ public ExecutionResult getExecutionResult() { return this.executionResult; } - public JsonObject getExecutionResultAsJsonObject() { - JsonObjectBuilder returnObjectBuilder = jsonObjectFactory.createObjectBuilder(); + public ObjectNode getExecutionResultAsJsonObject() { + ObjectNode returnObject = NODE_FACTORY.objectNode(); // Errors - returnObjectBuilder = addErrorsToResponse(returnObjectBuilder, executionResult); + addErrorsToResponse(returnObject, executionResult); // Data - returnObjectBuilder = addDataToResponse(returnObjectBuilder, executionResult); + addDataToResponse(returnObject, executionResult); // Extensions - returnObjectBuilder = addExtensionsToResponse(returnObjectBuilder, executionResult); + addExtensionsToResponse(returnObject, executionResult); - return returnObjectBuilder.build(); + return returnObject; + } + + /** + * Bridge method: converts the Jackson ObjectNode to a JSON-P JsonObject. + * This is needed for callers that still use JSON-P types (websocket handlers, JsonObjectResponseWriter). + * Will be removed when those callers are migrated to Jackson. + */ + public JsonObject getExecutionResultAsJsonPObject() { + String jsonString = getExecutionResultAsString(); + JsonProvider jsonProvider = JsonProvider.provider(); + try (StringReader sr = new StringReader(jsonString); + JsonReader reader = jsonProvider.createReader(sr)) { + return reader.readObject(); + } } public String getExecutionResultAsString() { - return getExecutionResultAsJsonObject().toString(); + try { + return OBJECT_MAPPER.writeValueAsString(getExecutionResultAsJsonObject()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } - private JsonObjectBuilder addErrorsToResponse(JsonObjectBuilder returnObjectBuilder, ExecutionResult executionResult) { + private void addErrorsToResponse(ObjectNode returnObject, ExecutionResult executionResult) { List errors = executionResult.getErrors(); if (errors != null) { - JsonArray jsonArray = errorsService.toJsonErrors(errors); - if (!jsonArray.isEmpty()) { - returnObjectBuilder = returnObjectBuilder.add(ERRORS, jsonArray); + ArrayNode jsonArray = errorsService.toJsonErrors(errors); + if (jsonArray.size() > 0) { + returnObject.set(ERRORS, jsonArray); } } - return returnObjectBuilder; } - private JsonObjectBuilder addDataToResponse(JsonObjectBuilder returnObjectBuilder, ExecutionResult executionResult) { + private void addDataToResponse(ObjectNode returnObject, ExecutionResult executionResult) { if (!executionResult.isDataPresent()) { - return returnObjectBuilder; + return; } Object pojoData = executionResult.getData(); - return addDataToResponse(returnObjectBuilder, pojoData); + addDataToResponse(returnObject, pojoData); } - private JsonObjectBuilder addDataToResponse(JsonObjectBuilder returnObjectBuilder, Object pojoData) { + private void addDataToResponse(ObjectNode returnObject, Object pojoData) { if (pojoData != null) { - JsonValue data = toJsonValue(pojoData); - return returnObjectBuilder.add(DATA, data); + JsonNode data = toJsonNode(pojoData); + returnObject.set(DATA, data); } else { - return returnObjectBuilder.addNull(DATA); + returnObject.putNull(DATA); } } - private JsonObjectBuilder addExtensionsToResponse(JsonObjectBuilder returnObjectBuilder, ExecutionResult executionResult) { + private void addExtensionsToResponse(ObjectNode returnObject, ExecutionResult executionResult) { final Map extensions = executionResult.getExtensions(); if (extensions != null) { // ERRORS - returnObjectBuilder = addExtensionToBuilder(extensions, returnObjectBuilder); + addExtensionToBuilder(extensions, returnObject); } else if (addedExtensions != null && !addedExtensions.isEmpty()) { // ADDED EXTENSIONS - returnObjectBuilder = addExtensionToBuilder(new HashMap(addedExtensions), returnObjectBuilder); + addExtensionToBuilder(new HashMap(addedExtensions), returnObject); } - return returnObjectBuilder; } - private JsonObjectBuilder addExtensionToBuilder(Map extensions, JsonObjectBuilder returnObjectBuilder) { - JsonObject extensionsObject = buildExtensions(extensions); - return returnObjectBuilder.add(EXTENSIONS, extensionsObject); + private void addExtensionToBuilder(Map extensions, ObjectNode returnObject) { + ObjectNode extensionsObject = buildExtensions(extensions); + returnObject.set(EXTENSIONS, extensionsObject); } - private JsonObject buildExtensions(final Map extensions) { - JsonObjectBuilder extensionsBuilder = jsonObjectFactory.createObjectBuilder(); + private ObjectNode buildExtensions(final Map extensions) { + ObjectNode extensionsNode = NODE_FACTORY.objectNode(); for (final Map.Entry entry : extensions.entrySet()) { if (entry.getKey() instanceof String) { String key = ((String) entry.getKey()); - final JsonValue value = toJsonValue(entry.getValue()); - extensionsBuilder.add(key, value); + final JsonNode value = toJsonNode(entry.getValue()); + extensionsNode.set(key, value); } } - return extensionsBuilder.build(); + return extensionsNode; } /** - * Build a JsonValue from the provided Object. + * Build a JsonNode from the provided Object. *

*

* GraphQL returns a limited set of values ({@code Collection}, {@code Map}, {@code Number}, {@code Boolean}, {@code Enum}), - * so the json value is build by hand. - * Additionally, {@code JsonB} is used as a fallback if an different type is encountered. + * so the json value is built by hand. + * Additionally, Jackson's {@code ObjectMapper} is used as a fallback if a different type is encountered. * * @param pojo a java object, limited to {@code Collection}, {@code Map}, {@code Number}, {@code Boolean} and {@code Enum} - * @return the json value + * @return the json node */ - private JsonValue toJsonValue(Object pojo) { + private JsonNode toJsonNode(Object pojo) { - final JsonValue jsonValue; if (pojo == null) { - return JsonValue.NULL; - } else if (pojo instanceof JsonValue) { - JsonValue value = (JsonValue) pojo; + return NullNode.getInstance(); + } else if (pojo instanceof JsonNode) { + JsonNode node = (JsonNode) pojo; if (Config.get().isExcludeNullFieldsInResponses()) { - return excludeNullFields(value); + return excludeNullFields(node); } else { - return value; + return node; } } else if (pojo instanceof Map) { - JsonObjectBuilder jsonObjectBuilder = jsonObjectFactory.createObjectBuilder(); + ObjectNode objectNode = NODE_FACTORY.objectNode(); Map map = (Map) pojo; map.forEach((key, value) -> { pushToPathBuffer(key); - JsonValue convertedValue = toJsonValue(value); - if ((!Config.get().isExcludeNullFieldsInResponses()) || (convertedValue != JsonValue.NULL + JsonNode convertedValue = toJsonNode(value); + if ((!Config.get().isExcludeNullFieldsInResponses()) || (!convertedValue.isNull() || errorPaths.contains(pathBuffer))) { - jsonObjectBuilder.add(key, convertedValue); + objectNode.set(key, convertedValue); } popFromThePathBuffer(); }); - jsonValue = jsonObjectBuilder.build(); + return objectNode; } else if (pojo instanceof Collection) { - Collection map = ((Collection) pojo); - JsonArrayBuilder builder = jsonObjectFactory.createArrayBuilder(); + Collection collection = ((Collection) pojo); + ArrayNode arrayNode = NODE_FACTORY.arrayNode(); int index = 0; - for (final Object o : map) { + for (final Object o : collection) { pushToPathBuffer(index); - builder.add(toJsonValue(o)); + arrayNode.add(toJsonNode(o)); popFromThePathBuffer(); index++; } - jsonValue = builder.build(); + return arrayNode; } else if (pojo instanceof Boolean) { - if (((Boolean) pojo)) { - jsonValue = JsonValue.TRUE; - } else { - jsonValue = JsonValue.FALSE; - } + return NODE_FACTORY.booleanNode((Boolean) pojo); } else if (pojo instanceof String) { - jsonValue = JSON_PROVIDER.createValue(((String) pojo)); + return NODE_FACTORY.textNode((String) pojo); } else if (pojo instanceof Double) { - jsonValue = JSON_PROVIDER.createValue(((Number) pojo).doubleValue()); + return NODE_FACTORY.numberNode((Double) pojo); } else if (pojo instanceof Float) { //upcast to double would lead to precision loss - jsonValue = JSON_PROVIDER.createValue(new BigDecimal(String.valueOf(((Number) pojo).floatValue()))); + return NODE_FACTORY.numberNode(new BigDecimal(String.valueOf(((Number) pojo).floatValue()))); } else if (pojo instanceof Long) { - jsonValue = JSON_PROVIDER.createValue(((Long) pojo)); + return NODE_FACTORY.numberNode((Long) pojo); } else if (pojo instanceof Integer || pojo instanceof Short || pojo instanceof Byte) { - jsonValue = JSON_PROVIDER.createValue(((Number) pojo).intValue()); + return NODE_FACTORY.numberNode(((Number) pojo).intValue()); } else if (pojo instanceof BigDecimal) { - jsonValue = JSON_PROVIDER.createValue(((BigDecimal) pojo)); + return NODE_FACTORY.numberNode((BigDecimal) pojo); } else if (pojo instanceof BigInteger) { - jsonValue = JSON_PROVIDER.createValue(((BigInteger) pojo)); + return NODE_FACTORY.numberNode((BigInteger) pojo); } else if (pojo instanceof Enum) { - jsonValue = JSON_PROVIDER.createValue(((Enum) pojo).name()); + return NODE_FACTORY.textNode(((Enum) pojo).name()); + } else if (pojo instanceof jakarta.json.JsonValue) { + return jsonPValueToJsonNode((jakarta.json.JsonValue) pojo); } else { - String json = jsonB.toJson(pojo); - try (StringReader sr = new StringReader(json); JsonReader reader = jsonReaderFactory.createReader(sr)) { - jsonValue = reader.readValue(); + try { + return OBJECT_MAPPER.valueToTree(pojo); + } catch (IllegalArgumentException e) { + return NODE_FACTORY.textNode(pojo.toString()); } } - - return jsonValue; } private void pushToPathBuffer(Object object) { @@ -235,28 +245,74 @@ private void popFromThePathBuffer() { } } - private static JsonValue excludeNullFields(JsonValue jsonValue) { - if (jsonValue instanceof JsonObject) { - JsonObject jsonObject = (JsonObject) jsonValue; - JsonObjectBuilder objectBuilder = jsonObjectFactory.createObjectBuilder(); - jsonObject.forEach((key, value) -> { - if (value != null && value.getValueType() != JsonValue.ValueType.NULL) { - objectBuilder.add(key, excludeNullFields(value)); + private static JsonNode jsonPValueToJsonNode(jakarta.json.JsonValue jsonPValue) { + switch (jsonPValue.getValueType()) { + case OBJECT: + ObjectNode objectNode = NODE_FACTORY.objectNode(); + jakarta.json.JsonObject jsonPObject = jsonPValue.asJsonObject(); + for (String key : jsonPObject.keySet()) { + objectNode.set(key, jsonPValueToJsonNode(jsonPObject.get(key))); + } + return objectNode; + case ARRAY: + ArrayNode arrayNode = NODE_FACTORY.arrayNode(); + for (jakarta.json.JsonValue item : jsonPValue.asJsonArray()) { + arrayNode.add(jsonPValueToJsonNode(item)); + } + return arrayNode; + case STRING: + return NODE_FACTORY.textNode(((jakarta.json.JsonString) jsonPValue).getString()); + case NUMBER: + jakarta.json.JsonNumber jsonPNumber = (jakarta.json.JsonNumber) jsonPValue; + if (jsonPNumber.isIntegral()) { + try { + return NODE_FACTORY.numberNode(jsonPNumber.longValueExact()); + } catch (ArithmeticException e) { + return NODE_FACTORY.numberNode(jsonPNumber.bigIntegerValue()); + } + } + return NODE_FACTORY.numberNode(jsonPNumber.bigDecimalValue()); + case TRUE: + return NODE_FACTORY.booleanNode(true); + case FALSE: + return NODE_FACTORY.booleanNode(false); + case NULL: + return NODE_FACTORY.nullNode(); + default: + return NODE_FACTORY.nullNode(); + } + } + + private static JsonNode excludeNullFields(JsonNode jsonNode) { + if (jsonNode instanceof ObjectNode) { + ObjectNode objectNode = (ObjectNode) jsonNode; + ObjectNode result = NODE_FACTORY.objectNode(); + objectNode.fields().forEachRemaining(entry -> { + if (entry.getValue() != null && !entry.getValue().isNull()) { + result.set(entry.getKey(), excludeNullFields(entry.getValue())); } }); - return objectBuilder.build(); - } else if (jsonValue instanceof JsonArray) { - JsonArray jsonArray = (JsonArray) jsonValue; - JsonArrayBuilder arrayBuilder = jsonObjectFactory.createArrayBuilder(); - for (JsonValue value : jsonArray) { - arrayBuilder.add(excludeNullFields(value)); + return result; + } else if (jsonNode instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) jsonNode; + ArrayNode result = NODE_FACTORY.arrayNode(); + for (JsonNode value : arrayNode) { + result.add(excludeNullFields(value)); } - return arrayBuilder.build(); + return result; } else { - return jsonValue; + return jsonNode; } } + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + mapper.disable(SerializationFeature.INDENT_OUTPUT); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return mapper; + } + private static final String DATA = "data"; private static final String ERRORS = "errors"; private static final String EXTENSIONS = "extensions"; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java index 1b753c49c..5096cc92b 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java @@ -454,12 +454,12 @@ private void logInput(String query, Optional> variables) { private void logOutput(ExecutionResponse executionResponse) { if (!payloadOption.equals(LogPayloadOption.off)) { - log.payloadOut(executionResponse.getExecutionResultAsJsonObject().toString()); + log.payloadOut(executionResponse.getExecutionResultAsString()); } // Also submit to the provided publisher if (traficPublisher.isPresent()) { - traficPublisher.get().submit("< " + executionResponse.getExecutionResultAsJsonObject().toString()); + traficPublisher.get().submit("< " + executionResponse.getExecutionResultAsString()); } } } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/JsonObjectResponseWriter.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/JsonObjectResponseWriter.java index fea066a2f..66af6f8d4 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/JsonObjectResponseWriter.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/JsonObjectResponseWriter.java @@ -52,7 +52,7 @@ public void logInput() { public void logOutput() { if (executionResponse != null) { - String prettyData = getPrettyJson(executionResponse.getExecutionResultAsJsonObject()); + String prettyData = getPrettyJson(executionResponse.getExecutionResultAsJsonPObject()); LOG.info(prettyData); } else if (throwable != null) { LOG.error("ERROR", throwable); @@ -65,7 +65,7 @@ public JsonObject getInput() { public JsonObject getOutput() { if (this.executionResponse != null) { - return this.executionResponse.getExecutionResultAsJsonObject(); + return this.executionResponse.getExecutionResultAsJsonPObject(); } return JsonObject.EMPTY_JSON_OBJECT; } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/DataFetcherException.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/DataFetcherException.java index aa6a31a08..9ce8508b8 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/DataFetcherException.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/DataFetcherException.java @@ -1,8 +1,8 @@ package io.smallrye.graphql.execution.datafetcher; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import io.smallrye.graphql.schema.model.Operation; @@ -12,17 +12,25 @@ * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class DataFetcherException extends RuntimeException { - private static final Jsonb JSONB = JsonbBuilder.create(new JsonbConfig().withFormatting(true)); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); public DataFetcherException() { } public DataFetcherException(Operation operation) { - super("Problem while fetching data for operation \n" + JSONB.toJson(operation)); + super("Problem while fetching data for operation \n" + toJson(operation)); } public DataFetcherException(Operation operation, Exception ex) { - super("Problem while fetching data for operation \n" + JSONB.toJson(operation), ex); + super("Problem while fetching data for operation \n" + toJson(operation), ex); } + private static String toJson(Operation operation) { + try { + return OBJECT_MAPPER.writeValueAsString(operation); + } catch (JsonProcessingException e) { + return operation.toString(); + } + } } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/ArgumentHelper.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/ArgumentHelper.java index ccc6d2e3d..67bb6d0e0 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/ArgumentHelper.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/ArgumentHelper.java @@ -17,14 +17,14 @@ import java.util.Optional; import java.util.Set; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.schema.DataFetchingEnvironment; import graphql.schema.GraphQLScalarType; import io.smallrye.graphql.execution.Classes; import io.smallrye.graphql.json.InputFieldsInfo; -import io.smallrye.graphql.json.JsonBCreator; +import io.smallrye.graphql.json.JacksonCreator; import io.smallrye.graphql.scalar.GraphQLScalarTypes; import io.smallrye.graphql.schema.model.AdaptWith; import io.smallrye.graphql.schema.model.Argument; @@ -213,6 +213,8 @@ private Object adaptInputWith(Field field, Object argumentValue, DataFetchingEnv argumentValue = correctComplexObjectFromMap((Map) argumentValue, field, dfe); } + argumentValue = coerceToParameterType(argumentValue, reflectionInvoker.getMethod()); + try { Object adaptedObject = reflectionInvoker.invoke(argumentValue); return adaptedObject; @@ -224,6 +226,36 @@ private Object adaptInputWith(Field field, Object argumentValue, DataFetchingEnv return argumentValue; } + private static Object coerceToParameterType(Object value, java.lang.reflect.Method method) { + if (value == null || method == null || method.getParameterCount() == 0) { + return value; + } + Class expected = method.getParameterTypes()[0]; + if (expected.isInstance(value)) { + return value; + } + if (value instanceof Number) { + Number num = (Number) value; + if (expected == Long.class || expected == long.class) { + return num.longValue(); + } else if (expected == Integer.class || expected == int.class) { + return num.intValue(); + } else if (expected == Short.class || expected == short.class) { + return num.shortValue(); + } else if (expected == Byte.class || expected == byte.class) { + return num.byteValue(); + } else if (expected == Double.class || expected == double.class) { + return num.doubleValue(); + } else if (expected == Float.class || expected == float.class) { + return num.floatValue(); + } + } + if (expected == String.class) { + return value.toString(); + } + return value; + } + private Object transformInput(Field field, Object object) throws AbstractDataFetcherException { if (object == null) { return null; @@ -299,7 +331,10 @@ private Object correctObjectClass(Object argumentValue, Field field, DataFetchin throws AbstractDataFetcherException { String receivedClassName = argumentValue.getClass().getName(); - if (Map.class.isAssignableFrom(argumentValue.getClass())) { + if (Map.class.isAssignableFrom(argumentValue.getClass()) + && isJacksonJsonNodeType(field.getReference().getClassName())) { + return JacksonCreator.getObjectMapper().valueToTree(argumentValue); + } else if (Map.class.isAssignableFrom(argumentValue.getClass())) { return correctComplexObjectFromMap((Map) argumentValue, field, dfe); } else if (receivedClassName.equals(String.class.getName())) { // Edge case for ObjectId: If the field is of type org.bson.types.ObjectId, return the argument value. @@ -333,6 +368,12 @@ private Object correctObjectClass(Object argumentValue, Field field, DataFetchin * @param field the field as created while scanning * @return a java object of this type. */ + private static boolean isJacksonJsonNodeType(String className) { + return "com.fasterxml.jackson.databind.JsonNode".equals(className) + || "com.fasterxml.jackson.databind.node.ObjectNode".equals(className) + || "com.fasterxml.jackson.databind.node.ArrayNode".equals(className); + } + private Object correctComplexObjectFromMap(Map m, Field field, DataFetchingEnvironment dfe) throws AbstractDataFetcherException { String className = field.getReference().getClassName(); @@ -368,6 +409,11 @@ private Object correctComplexObjectFromMap(Map m, Field field, DataFetchingEnvir } } + // Save adapted map fields to set them after JSON round-trip. + // Complex-keyed maps can't survive JSON serialization because Jackson + // serializes Map keys using toString(), losing the original key data. + Map deferredMapFields = new HashMap<>(); + if (InputFieldsInfo.hasAdaptWithFields(className)) { Map adaptingFields = InputFieldsInfo.getAdaptWithFields(className); @@ -377,7 +423,14 @@ private Object correctComplexObjectFromMap(Map m, Field field, DataFetchingEnvir Object valueThatShouldAdapt = m.get(fieldName); Field fieldThatShouldAdapt = entry.getValue(); Object valueThatAdapted = super.recursiveAdapting(valueThatShouldAdapt, fieldThatShouldAdapt, dfe); - m.put(fieldName, valueThatAdapted); + if (fieldThatShouldAdapt.hasWrapper() && fieldThatShouldAdapt.getWrapper().isMap()) { + // Defer map fields: remove from the map before JSON round-trip + // so Jackson doesn't try to serialize complex map keys via toString() + deferredMapFields.put(fieldThatShouldAdapt.getPropertyName(), valueThatAdapted); + m.remove(fieldName); + } else { + m.put(fieldName, valueThatAdapted); + } } } } @@ -386,8 +439,28 @@ private Object correctComplexObjectFromMap(Map m, Field field, DataFetchingEnvir m = includeNullCreatorParameters(m, field); // Create a valid jsonString from a map - String jsonString = JsonBCreator.getJsonB(className).toJson(m); - return correctComplexObjectFromJsonString(jsonString, field); + try { + String jsonString = JacksonCreator.getObjectMapper(className).writeValueAsString(m); + Object result = correctComplexObjectFromJsonString(jsonString, field); + + // Set deferred map fields directly via reflection + if (!deferredMapFields.isEmpty()) { + Class resultClass = result.getClass(); + for (Map.Entry deferred : deferredMapFields.entrySet()) { + try { + java.lang.reflect.Field javaField = findField(resultClass, deferred.getKey()); + javaField.setAccessible(true); + javaField.set(result, deferred.getValue()); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Failed to set deferred map field: " + deferred.getKey(), e); + } + } + } + + return result; + } catch (JsonProcessingException e) { + throw new TransformException(e, field, m); + } } /** @@ -448,10 +521,10 @@ private Object correctComplexObjectFromJsonString(String jsonString, Field field } try { - Jsonb jsonb = JsonBCreator.getJsonB(className); - return jsonb.fromJson(jsonString, type); - } catch (JsonbException jbe) { - throw new TransformException(jbe, field, jsonString); + ObjectMapper objectMapper = JacksonCreator.getObjectMapper(className); + return objectMapper.readValue(jsonString, objectMapper.constructType(type)); + } catch (JsonProcessingException jpe) { + throw new TransformException(jpe, field, jsonString); } } @@ -502,5 +575,20 @@ public Type[] getActualTypeArguments() { } } + /** + * Find a declared field in the class hierarchy. + */ + private static java.lang.reflect.Field findField(Class clazz, String fieldName) throws NoSuchFieldException { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } + private static final String CONSTRUCTOR_METHOD_NAME = ""; } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java index 9d86800fb..55519e079 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/helper/DefaultMapAdapter.java @@ -6,11 +6,11 @@ import java.util.Map; import java.util.Set; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.smallrye.graphql.api.Entry; -import io.smallrye.graphql.json.JsonBCreator; +import io.smallrye.graphql.json.JacksonCreator; import io.smallrye.graphql.schema.model.Field; import io.smallrye.graphql.schema.model.Reference; import io.smallrye.graphql.schema.model.ReferenceType; @@ -75,13 +75,13 @@ private T toObject(Reference reference, T t) { ReferenceType type = reference.getType(); String className = reference.getClassName(); if (!type.equals(ReferenceType.SCALAR)) { - String jsonString = JsonBCreator.getJsonB().toJson(t); try { - Jsonb jsonb = JsonBCreator.getJsonB(className); + String jsonString = JacksonCreator.getObjectMapper().writeValueAsString(t); + ObjectMapper objectMapper = JacksonCreator.getObjectMapper(className); Class clazz = classloadingService.loadClass(className); - return (T) jsonb.fromJson(jsonString, clazz); - } catch (JsonbException jbe) { - throw new RuntimeException(jbe); + return (T) objectMapper.readValue(jsonString, clazz); + } catch (JsonProcessingException jpe) { + throw new RuntimeException(jpe); } } return t; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorCodeExtensionProvider.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorCodeExtensionProvider.java index c8afb4fba..a2365da3d 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorCodeExtensionProvider.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ErrorCodeExtensionProvider.java @@ -1,15 +1,18 @@ package io.smallrye.graphql.execution.error; -import static io.smallrye.graphql.JsonProviderHolder.JSON_PROVIDER; import static java.util.Locale.ROOT; import jakarta.json.JsonValue; +import jakarta.json.spi.JsonProvider; import io.smallrye.graphql.api.ErrorExtensionProvider; import io.smallrye.graphql.schema.model.ErrorInfo; import io.smallrye.graphql.spi.config.Config; public class ErrorCodeExtensionProvider implements ErrorExtensionProvider { + + private static final JsonProvider JSON_PROVIDER = JsonProvider.provider(); + @Override public String getKey() { return Config.ERROR_EXTENSION_CODE; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExceptionNameErrorExtensionProvider.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExceptionNameErrorExtensionProvider.java index 5438a1b47..edb477cd3 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExceptionNameErrorExtensionProvider.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExceptionNameErrorExtensionProvider.java @@ -1,13 +1,15 @@ package io.smallrye.graphql.execution.error; -import static io.smallrye.graphql.JsonProviderHolder.JSON_PROVIDER; - import jakarta.json.JsonString; +import jakarta.json.spi.JsonProvider; import io.smallrye.graphql.api.ErrorExtensionProvider; import io.smallrye.graphql.spi.config.Config; public class ExceptionNameErrorExtensionProvider implements ErrorExtensionProvider { + + private static final JsonProvider JSON_PROVIDER = JsonProvider.provider(); + @Override public String getKey() { return Config.ERROR_EXTENSION_EXCEPTION; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java index 0a1f97b73..1d074ec64 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java @@ -1,23 +1,16 @@ package io.smallrye.graphql.execution.error; -import static io.smallrye.graphql.JsonProviderHolder.JSON_PROVIDER; - -import java.io.StringReader; import java.util.List; import java.util.Map; import java.util.Optional; -import jakarta.json.JsonArray; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; import jakarta.json.JsonValue; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; import graphql.ExceptionWhileDataFetching; import graphql.GraphQLError; @@ -31,38 +24,29 @@ */ public class ExecutionErrorsService { - private static final JsonBuilderFactory jsonBuilderFactory = JSON_PROVIDER.createBuilderFactory(null); - private static final JsonReaderFactory jsonReaderFactory = JSON_PROVIDER.createReaderFactory(null); - private static final Jsonb JSONB = JsonbBuilder.create(new JsonbConfig() - .withNullValues(Boolean.TRUE) - .withFormatting(Boolean.TRUE)); + private static final JsonNodeFactory NODE_FACTORY = JsonNodeFactory.instance; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final ErrorExtensionProviders errorExtensionProviders = new ErrorExtensionProviders(); private final Config config = Config.get(); - public JsonArray toJsonErrors(List errors) { - JsonArrayBuilder arrayBuilder = jsonBuilderFactory.createArrayBuilder(); + public ArrayNode toJsonErrors(List errors) { + ArrayNode arrayNode = NODE_FACTORY.arrayNode(); for (GraphQLError e : errors) { - arrayBuilder.add(toJsonError(e)); + arrayNode.add(toJsonError(e)); } - return arrayBuilder.build(); + return arrayNode; } - private JsonObject toJsonError(GraphQLError error) { - String json = JSONB.toJson(error.toSpecification()); - try (StringReader sr = new StringReader(json); JsonReader reader = jsonReaderFactory.createReader(sr)) { - - JsonObject jsonErrors = reader.readObject(); + private ObjectNode toJsonError(GraphQLError error) { + ObjectNode jsonErrors = OBJECT_MAPPER.valueToTree(error.toSpecification()); - JsonObjectBuilder resultBuilder = jsonBuilderFactory.createObjectBuilder(jsonErrors); - - getOptionalExtensions(error).ifPresent(jsonObject -> resultBuilder.add(EXTENSIONS, jsonObject)); - return resultBuilder.build(); - } + getOptionalExtensions(error).ifPresent(extensionsNode -> jsonErrors.set(EXTENSIONS, extensionsNode)); + return jsonErrors; } - private Optional getOptionalExtensions(GraphQLError error) { + private Optional getOptionalExtensions(GraphQLError error) { if (error instanceof ValidationError) { return getValidationExtensions((ValidationError) error); } else if (error instanceof ExceptionWhileDataFetching) { @@ -71,82 +55,107 @@ private Optional getOptionalExtensions(GraphQLError error) { return Optional.empty(); } - private Optional getValidationExtensions(ValidationError error) { + private Optional getValidationExtensions(ValidationError error) { if (config.getErrorExtensionFields().isPresent()) { - JsonObjectBuilder objectBuilder = jsonBuilderFactory.createObjectBuilder(); - addKeyValue(objectBuilder, Config.ERROR_EXTENSION_DESCRIPTION, error.getDescription()); - addKeyValue(objectBuilder, Config.ERROR_EXTENSION_VALIDATION_ERROR_TYPE, error.getValidationErrorType().toString()); - objectBuilder.add(Config.ERROR_EXTENSION_QUERY_PATH, toJsonArray(error.getQueryPath())); - addKeyValue(objectBuilder, Config.ERROR_EXTENSION_CLASSIFICATION, error.getErrorType().toString()); + ObjectNode objectNode = NODE_FACTORY.objectNode(); + addKeyValue(objectNode, Config.ERROR_EXTENSION_DESCRIPTION, error.getDescription()); + addKeyValue(objectNode, Config.ERROR_EXTENSION_VALIDATION_ERROR_TYPE, error.getValidationErrorType().toString()); + objectNode.set(Config.ERROR_EXTENSION_QUERY_PATH, toJsonArray(error.getQueryPath())); + addKeyValue(objectNode, Config.ERROR_EXTENSION_CLASSIFICATION, error.getErrorType().toString()); Map extensions = error.getExtensions(); - populateCustomExtensions(objectBuilder, extensions); - return Optional.of(objectBuilder.build()); + populateCustomExtensions(objectNode, extensions); + return Optional.of(objectNode); } return Optional.empty(); } - private Optional getDataFetchingExtensions(ExceptionWhileDataFetching error) { + private Optional getDataFetchingExtensions(ExceptionWhileDataFetching error) { if (config.getErrorExtensionFields().isPresent()) { Throwable exception = error.getException(); - JsonObjectBuilder objectBuilder = jsonBuilderFactory.createObjectBuilder(); - addKeyValue(objectBuilder, Config.ERROR_EXTENSION_CLASSIFICATION, error.getErrorType().toString()); - addErrorExtensions(objectBuilder, exception); + ObjectNode objectNode = NODE_FACTORY.objectNode(); + addKeyValue(objectNode, Config.ERROR_EXTENSION_CLASSIFICATION, error.getErrorType().toString()); + addErrorExtensions(objectNode, exception); Map extensions = error.getExtensions(); - populateCustomExtensions(objectBuilder, extensions); + populateCustomExtensions(objectNode, extensions); - return Optional.of(objectBuilder.build()); + return Optional.of(objectNode); } return Optional.empty(); } - private void addErrorExtensions(JsonObjectBuilder objectBuilder, Throwable exception) { + private void addErrorExtensions(ObjectNode objectNode, Throwable exception) { errorExtensionProviders.get() - .forEach(provider -> addKeyValue(objectBuilder, provider.getKey(), provider.mapValueFrom(exception))); + .forEach(provider -> { + JsonValue jsonValue = provider.mapValueFrom(exception); + // Convert jakarta.json.JsonValue to Jackson JsonNode + JsonNode jacksonNode = convertJsonValueToJsonNode(jsonValue); + addKeyValue(objectNode, provider.getKey(), jacksonNode); + }); + } + + /** + * Bridge method: converts a jakarta.json.JsonValue to a Jackson JsonNode. + * This is needed because ErrorExtensionProvider (in server/api) returns JSON-P types. + */ + private JsonNode convertJsonValueToJsonNode(JsonValue jsonValue) { + if (jsonValue == null) { + return NODE_FACTORY.nullNode(); + } + try { + // Use the JSON-P toString() which produces valid JSON, then parse with Jackson + return OBJECT_MAPPER.readTree(jsonValue.toString()); + } catch (Exception e) { + // fallback: treat as string + return NODE_FACTORY.textNode(jsonValue.toString()); + } } - private void populateCustomExtensions(JsonObjectBuilder objectBuilder, Map extensions) { + private void populateCustomExtensions(ObjectNode objectNode, Map extensions) { if (extensions != null) { for (Map.Entry entry : extensions.entrySet()) { if (config.getErrorExtensionFields().isEmpty() || (config.getErrorExtensionFields().isPresent() && config.getErrorExtensionFields().get().contains(entry.getKey()))) { Object value = entry.getValue(); - if (value instanceof JsonValue) - addKeyValue(objectBuilder, entry.getKey(), (JsonValue) value); - else if (value instanceof Map) - addKeyValue(objectBuilder, entry.getKey(), JSON_PROVIDER.createObjectBuilder((Map) value).build()); - else - addKeyValue(objectBuilder, entry.getKey(), value != null ? value.toString() : null); + if (value instanceof JsonNode) { + addKeyValue(objectNode, entry.getKey(), (JsonNode) value); + } else if (value instanceof JsonValue) { + addKeyValue(objectNode, entry.getKey(), convertJsonValueToJsonNode((JsonValue) value)); + } else if (value instanceof Map) { + addKeyValue(objectNode, entry.getKey(), OBJECT_MAPPER.valueToTree(value)); + } else { + addKeyValue(objectNode, entry.getKey(), value != null ? value.toString() : null); + } } } } } - private JsonArray toJsonArray(List list) { - JsonArrayBuilder arrayBuilder = jsonBuilderFactory.createArrayBuilder(); + private ArrayNode toJsonArray(List list) { + ArrayNode arrayNode = NODE_FACTORY.arrayNode(); if (list != null && !list.isEmpty()) { for (Object o : list) { if (o != null) - arrayBuilder.add(o.toString()); + arrayNode.add(o.toString()); } } - return arrayBuilder.build(); + return arrayNode; } - private void addKeyValue(JsonObjectBuilder objectBuilder, String key, String value) { - addKeyValue(objectBuilder, key, JSON_PROVIDER.createValue(value)); + private void addKeyValue(ObjectNode objectNode, String key, String value) { + addKeyValue(objectNode, key, value != null ? NODE_FACTORY.textNode(value) : NODE_FACTORY.nullNode()); } - private void addKeyValue(JsonObjectBuilder objectBuilder, String key, JsonValue value) { - value = value != null ? value : JsonValue.NULL; + private void addKeyValue(ObjectNode objectNode, String key, JsonNode value) { + value = value != null ? value : NODE_FACTORY.nullNode(); if (config.getErrorExtensionFields().isPresent()) { List fieldsThatShouldBeIncluded = config.getErrorExtensionFields().get(); if (fieldsThatShouldBeIncluded.contains(key)) { - objectBuilder.add(key, value); + objectNode.set(key, value); } } else { - objectBuilder.add(key, value); + objectNode.set(key, value); } } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java index ba8992284..57a17f7b8 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java @@ -10,10 +10,11 @@ import java.util.ServiceLoader; import jakarta.annotation.Priority; -import jakarta.json.bind.Jsonb; import org.jboss.logging.Logger; +import com.fasterxml.jackson.databind.ObjectMapper; + import graphql.GraphQL; import graphql.schema.GraphQLSchema; import io.smallrye.graphql.api.Context; @@ -176,16 +177,16 @@ public Operation fireCreateOperation(Operation operation) { return operation; } - public Map fireOverrideJsonbConfig() { - Map overrides = new HashMap<>(); + public Map fireOverrideObjectMapperConfig() { + Map overrides = new HashMap<>(); for (EventingService extensionService : enabledServices) { - Map map = extensionService.overrideJsonbConfig(); - map.forEach((clazz, jsonb) -> { - LOG.debug("Registering custom JsonB config for class " + clazz + " (it was returned by " + Map map = extensionService.overrideObjectMapperConfig(); + map.forEach((clazz, objectMapper) -> { + LOG.debug("Registering custom ObjectMapper config for class " + clazz + " (it was returned by " + extensionService.getClass() .getName() + ")"); - overrides.put(clazz, jsonb); + overrides.put(clazz, objectMapper); }); } return overrides; diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/GraphQLNamingStrategy.java b/server/implementation/src/main/java/io/smallrye/graphql/json/GraphQLNamingStrategy.java index abfa236dd..10e1ce246 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/json/GraphQLNamingStrategy.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/json/GraphQLNamingStrategy.java @@ -2,14 +2,14 @@ import java.util.Map; -import jakarta.json.bind.config.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; /** - * Naming strategy that take GraphQL annotations into account + * Naming strategy that takes GraphQL annotations into account * * @author Phillip Kruger (phillip.kruger@redhat.com) */ -public class GraphQLNamingStrategy implements PropertyNamingStrategy { +public class GraphQLNamingStrategy extends PropertyNamingStrategies.NamingBase { private final Map customFieldNameMapping; public GraphQLNamingStrategy(Map customFieldNameMapping) { @@ -17,11 +17,8 @@ public GraphQLNamingStrategy(Map customFieldNameMapping) { } @Override - public String translateName(String string) { + public String translate(String name) { // Name mapping for input transformation - if (customFieldNameMapping.containsKey(string)) { - return customFieldNameMapping.get(string); - } - return string; + return customFieldNameMapping.getOrDefault(name, name); } } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/InputFieldsInfo.java b/server/implementation/src/main/java/io/smallrye/graphql/json/InputFieldsInfo.java index 8e63bbabe..329a9eaf0 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/json/InputFieldsInfo.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/json/InputFieldsInfo.java @@ -44,9 +44,8 @@ protected static void register(InputType inputType) { && !field.getTransformation().isJsonB()) { fieldsThatNeedsTransformation.put(field.getName(), field); } - // See if there is a adapter - if (field.isAdaptingWith() - && !field.getAdaptWith().isJsonB()) { + // See if there is an adapter + if (field.isAdaptingWith()) { fieldsThatNeedsAdaptingWith.put(field.getName(), field); // See if there is a map (default adapter) } else if (field.hasWrapper() && field.getWrapper().isMap()) { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/JacksonCreator.java b/server/implementation/src/main/java/io/smallrye/graphql/json/JacksonCreator.java new file mode 100644 index 000000000..a9763b226 --- /dev/null +++ b/server/implementation/src/main/java/io/smallrye/graphql/json/JacksonCreator.java @@ -0,0 +1,305 @@ +package io.smallrye.graphql.json; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.KeyDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.smallrye.graphql.api.CustomFloatScalar; +import io.smallrye.graphql.api.CustomIntScalar; +import io.smallrye.graphql.api.CustomStringScalar; +import io.smallrye.graphql.jackson.jsonb.JsonbCompatModule; +import io.smallrye.graphql.schema.model.Field; +import io.smallrye.graphql.schema.model.InputType; +import io.smallrye.graphql.spi.ClassloadingService; + +/** + * Here we create ObjectMapper instances for certain input objects. + * + * We only use Jackson on input, as output uses data fetchers per field. + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public class JacksonCreator { + + private static final SimpleModule CUSTOM_SCALARS_MODULE = createCustomScalarsModule(); + private static final ObjectMapper OBJECT_MAPPER = createDefaultObjectMapper(); + + private static final Map mapperMap = new HashMap<>(); + + private JacksonCreator() { + } + + protected static void register(InputType inputType) { + if (inputType.hasFields()) { + Map namemapping = new HashMap<>(); + Collection fields = inputType.getFields().values(); + for (Field field : fields) { + // See if the graphql name and property name is different + if (!field.getName().equals(field.getPropertyName())) { + namemapping.put(field.getPropertyName(), field.getName()); + } + } + + // Seems like there are some name mapping needed + if (!namemapping.isEmpty()) { + ObjectMapper mapper = createObjectMapper(namemapping); + mapperMap.put(inputType.getClassName(), mapper); + } + } + } + + protected static void override(Map overrides) { + mapperMap.putAll(overrides); + } + + public static ObjectMapper getObjectMapper(String className) { + if (mapperMap.containsKey(className)) { + return mapperMap.get(className); + } + return getObjectMapper(); // default vanilla + } + + public static ObjectMapper getObjectMapper() { + return OBJECT_MAPPER; + } + + private static ObjectMapper createObjectMapper(Map customFieldNameMapping) { + ObjectMapper mapper = createDefaultObjectMapper(); + mapper.setPropertyNamingStrategy(new GraphQLNamingStrategy(customFieldNameMapping)); + return mapper; + } + + private static ObjectMapper createDefaultObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JsonbCompatModule()); + mapper.registerModule(new JavaTimeModule()); + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(CUSTOM_SCALARS_MODULE); + mapper.registerModule(createComplexKeyModule()); + mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + // Include null values in serialization (required by @JsonbCreator / Jackson creator methods) + mapper.setDefaultPropertyInclusion(JsonInclude.Include.ALWAYS); + return mapper; + } + + private static SimpleModule createCustomScalarsModule() { + SimpleModule module = new SimpleModule("CustomScalars"); + + // CustomStringScalar serializer/deserializer + module.addSerializer(CustomStringScalar.class, new JsonSerializer() { + @Override + public void serialize(CustomStringScalar value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(value.stringValueForSerialization()); + } + }); + module.addDeserializer(CustomStringScalar.class, new JsonDeserializer() { + @Override + public CustomStringScalar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + if (node == null || node.isNull()) { + return null; + } + ClassloadingService classloadingService = ClassloadingService.get(); + try { + Class targetType = classloadingService + .loadClass(ctxt.getContextualType() != null + ? ctxt.getContextualType().getRawClass().getName() + : CustomStringScalar.class.getName()); + return (CustomStringScalar) targetType + .getConstructor(String.class) + .newInstance(node.asText()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + + // CustomIntScalar serializer/deserializer + module.addSerializer(CustomIntScalar.class, new JsonSerializer() { + @Override + public void serialize(CustomIntScalar value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeNumber(value.intValueForSerialization()); + } + }); + module.addDeserializer(CustomIntScalar.class, new JsonDeserializer() { + @Override + public CustomIntScalar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + if (node == null || node.isNull()) { + return null; + } + ClassloadingService classloadingService = ClassloadingService.get(); + try { + Class targetType = classloadingService + .loadClass(ctxt.getContextualType() != null + ? ctxt.getContextualType().getRawClass().getName() + : CustomIntScalar.class.getName()); + return (CustomIntScalar) targetType + .getConstructor(Integer.class) + .newInstance(node.intValue()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + + // CustomFloatScalar serializer/deserializer + module.addSerializer(CustomFloatScalar.class, new JsonSerializer() { + @Override + public void serialize(CustomFloatScalar value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeNumber(value.floatValueForSerialization()); + } + }); + module.addDeserializer(CustomFloatScalar.class, new JsonDeserializer() { + @Override + public CustomFloatScalar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + if (node == null || node.isNull()) { + return null; + } + ClassloadingService classloadingService = ClassloadingService.get(); + try { + Class targetType = classloadingService + .loadClass(ctxt.getContextualType() != null + ? ctxt.getContextualType().getRawClass().getName() + : CustomFloatScalar.class.getName()); + return (CustomFloatScalar) targetType + .getConstructor(BigDecimal.class) + .newInstance(node.decimalValue()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + + // jakarta.json.JsonObject deserializer — needed for JSON scalar type support + module.addDeserializer(jakarta.json.JsonObject.class, new JsonDeserializer() { + @Override + public jakarta.json.JsonObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + return (jakarta.json.JsonObject) jacksonNodeToJsonPValue(node); + } + }); + + // jakarta.json.JsonArray deserializer + module.addDeserializer(jakarta.json.JsonArray.class, new JsonDeserializer() { + @Override + public jakarta.json.JsonArray deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + return (jakarta.json.JsonArray) jacksonNodeToJsonPValue(node); + } + }); + + // jakarta.json.JsonValue deserializer + module.addDeserializer(jakarta.json.JsonValue.class, new JsonDeserializer() { + @Override + public jakarta.json.JsonValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + return jacksonNodeToJsonPValue(node); + } + }); + + return module; + } + + private static com.fasterxml.jackson.databind.Module createComplexKeyModule() { + return new com.fasterxml.jackson.databind.Module() { + @Override + public String getModuleName() { + return "ComplexMapKeys"; + } + + @Override + public com.fasterxml.jackson.core.Version version() { + return com.fasterxml.jackson.core.Version.unknownVersion(); + } + + @Override + public void setupModule(SetupContext context) { + context.addKeyDeserializers(new KeyDeserializers() { + @Override + public KeyDeserializer findKeyDeserializer(JavaType keyType, DeserializationConfig config, + BeanDescription beanDesc) throws JsonMappingException { + Class raw = keyType.getRawClass(); + if (!isComplexKeyType(raw)) { + return null; + } + return new KeyDeserializer() { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) ctxt.getParser().getCodec(); + return mapper.readValue(key, keyType); + } + }; + } + }); + } + }; + } + + private static boolean isComplexKeyType(Class type) { + if (type.isPrimitive() || type.isEnum() || type.isArray()) { + return false; + } + String name = type.getName(); + return !name.startsWith("java.") && !name.startsWith("javax.") + && !name.startsWith("jakarta.") && !name.startsWith("com.fasterxml."); + } + + private static jakarta.json.JsonValue jacksonNodeToJsonPValue(JsonNode node) { + if (node == null || node.isNull()) { + return jakarta.json.JsonValue.NULL; + } + if (node.isObject()) { + jakarta.json.JsonObjectBuilder builder = jakarta.json.Json.createObjectBuilder(); + node.fields().forEachRemaining(entry -> builder.add(entry.getKey(), jacksonNodeToJsonPValue(entry.getValue()))); + return builder.build(); + } + if (node.isArray()) { + jakarta.json.JsonArrayBuilder builder = jakarta.json.Json.createArrayBuilder(); + node.forEach(item -> builder.add(jacksonNodeToJsonPValue(item))); + return builder.build(); + } + if (node.isTextual()) { + return jakarta.json.Json.createValue(node.asText()); + } + if (node.isBoolean()) { + return node.asBoolean() ? jakarta.json.JsonValue.TRUE : jakarta.json.JsonValue.FALSE; + } + if (node.isIntegralNumber()) { + return jakarta.json.Json.createValue(node.longValue()); + } + if (node.isFloatingPointNumber()) { + return jakarta.json.Json.createValue(node.decimalValue()); + } + return jakarta.json.JsonValue.NULL; + } +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java deleted file mode 100644 index 7f4ee2f2a..000000000 --- a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonBCreator.java +++ /dev/null @@ -1,214 +0,0 @@ -package io.smallrye.graphql.json; - -import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_FLOAT_DESERIALIZER; -import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_FLOAT_SERIALIZER; -import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_INT_DESERIALIZER; -import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_INT_SERIALIZER; -import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_STRING_DESERIALIZER; -import static io.smallrye.graphql.json.JsonBCreator.CustomScalarSerializers.CUSTOM_STRING_SERIALIZER; - -import java.lang.reflect.Type; -import java.math.BigDecimal; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import jakarta.json.JsonValue.ValueType; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; -import jakarta.json.bind.serializer.DeserializationContext; -import jakarta.json.bind.serializer.JsonbDeserializer; -import jakarta.json.bind.serializer.JsonbSerializer; -import jakarta.json.bind.serializer.SerializationContext; -import jakarta.json.stream.JsonGenerator; -import jakarta.json.stream.JsonParser; - -import io.smallrye.graphql.api.CustomFloatScalar; -import io.smallrye.graphql.api.CustomIntScalar; -import io.smallrye.graphql.api.CustomStringScalar; -import io.smallrye.graphql.schema.model.Field; -import io.smallrye.graphql.schema.model.InputType; -import io.smallrye.graphql.spi.ClassloadingService; - -/** - * Here we create JsonB Objects for certain input object. - * - * We only use JsonB on input, as output use data fetchers per field. - * - * @author Phillip Kruger (phillip.kruger@redhat.com) - */ -public class JsonBCreator { - private static final Jsonb JSONB = JsonbBuilder.create(new JsonbConfig() - .withFormatting(true) - .withNullValues(true) //null values are required by @JsonbCreator - .withSerializers(CUSTOM_STRING_SERIALIZER, - CUSTOM_INT_SERIALIZER, - CUSTOM_FLOAT_SERIALIZER) - .withDeserializers(CUSTOM_STRING_DESERIALIZER, - CUSTOM_INT_DESERIALIZER, - CUSTOM_FLOAT_DESERIALIZER)); - - private static final Map jsonMap = new HashMap<>(); - - private JsonBCreator() { - } - - protected static void register(InputType inputType) { - if (inputType.hasFields()) { - Map namemapping = new HashMap<>(); - Collection fields = inputType.getFields().values(); - for (Field field : fields) { - // See if the graphql name and property name is different - if (!field.getName().equals(field.getPropertyName())) { - namemapping.put(field.getPropertyName(), field.getName()); - } - } - - // Seems like there are some name mapping needed - if (!namemapping.isEmpty()) { - Jsonb jsonB = createJsonB(namemapping); - jsonMap.put(inputType.getClassName(), jsonB); - } - } - } - - protected static void override(Map overrides) { - jsonMap.putAll(overrides); - } - - public static Jsonb getJsonB(String className) { - if (jsonMap.containsKey(className)) { - return jsonMap.get(className); - } - return getJsonB(); // default vanilla - } - - public static Jsonb getJsonB() { - return JSONB; - } - - private static Jsonb createJsonB(Map customFieldNameMapping) { - - JsonbConfig config = createDefaultConfig() - .withPropertyNamingStrategy(new GraphQLNamingStrategy(customFieldNameMapping)); - - return JsonbBuilder.create(config); - } - - private static JsonbConfig createDefaultConfig() { - return new JsonbConfig() - .withNullValues(Boolean.TRUE) - .withFormatting(Boolean.TRUE); - } - - static class CustomScalarSerializers { - // Note: using lambdas for the SERIALIZER/DESERIALIZER instances doesn't work because it - // hides the parameterized type from Jsonb. - - /** - * A serializer for CustomScalars based on GraphQL Strings, to inform JsonB how to serialize - * a CustomStringScalar to a String value. - */ - static JsonbSerializer CUSTOM_STRING_SERIALIZER = new JsonbSerializer<>() { - @Override - public void serialize(CustomStringScalar customStringScalar, JsonGenerator jsonGenerator, - SerializationContext serializationContext) { - jsonGenerator.write(customStringScalar.stringValueForSerialization()); - } - }; - - /** - * A deserializer for CustomScalars based on GraphQL Strings, to inform JsonB how to - * deserialize to an instance of a CustomStringScalar. - */ - static JsonbDeserializer CUSTOM_STRING_DESERIALIZER = new JsonbDeserializer<>() { - @Override - public CustomStringScalar deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext, Type type) { - ClassloadingService classloadingService = ClassloadingService.get(); - try { - if (jsonParser.getValue().getValueType() == ValueType.NULL) { - return null; - } else { - return (CustomStringScalar) classloadingService.loadClass(type.getTypeName()) - .getConstructor(String.class) - .newInstance(jsonParser.getString()); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - - /** - * A serializer for CustomScalars based on a GraphQL Int, to inform JsonB how to serialize - * a CustomStringScalar to a BigInteger value. - */ - static JsonbSerializer CUSTOM_INT_SERIALIZER = new JsonbSerializer<>() { - @Override - public void serialize(CustomIntScalar customIntScalar, JsonGenerator jsonGenerator, - SerializationContext serializationContext) { - jsonGenerator.write(customIntScalar.intValueForSerialization()); - } - }; - - /** - * A deserializer for CustomScalars based on a GraphQL Int, to inform JsonB how to - * deserialize to an instance of a CustomIntScalar. - */ - static JsonbDeserializer CUSTOM_INT_DESERIALIZER = new JsonbDeserializer<>() { - @Override - public CustomIntScalar deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext, Type type) { - ClassloadingService classloadingService = ClassloadingService.get(); - try { - if (jsonParser.getValue().getValueType() == ValueType.NULL) { - return null; - } else { - return (CustomIntScalar) classloadingService.loadClass(type.getTypeName()) - .getConstructor(Integer.class) - .newInstance(jsonParser.getInt()); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - - /** - * A serializer for CustomScalars based on a GraphQL Float, to inform JsonB how to serialize - * a CustomStringScalar to a BigDecimal value. - */ - static JsonbSerializer CUSTOM_FLOAT_SERIALIZER = new JsonbSerializer<>() { - @Override - public void serialize(CustomFloatScalar customFloatScalar, JsonGenerator jsonGenerator, - SerializationContext serializationContext) { - jsonGenerator.write(customFloatScalar.floatValueForSerialization()); - } - }; - - /** - * A deserializer for CustomScalars based on a GraphQL Float, to inform JsonB how to - * deserialize to an instance of a CustomFloatScalar. - */ - static JsonbDeserializer CUSTOM_FLOAT_DESERIALIZER = new JsonbDeserializer<>() { - @Override - public CustomFloatScalar deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext, Type type) { - ClassloadingService classloadingService = ClassloadingService.get(); - try { - if (jsonParser.getValue().getValueType() == ValueType.NULL) { - return null; - } else { - return (CustomFloatScalar) classloadingService.loadClass(type.getTypeName()) - .getConstructor(BigDecimal.class) - .newInstance(jsonParser.getBigDecimal()); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - } -} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonInputRegistry.java b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonInputRegistry.java index 3f044b4c5..68710611a 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/json/JsonInputRegistry.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/json/JsonInputRegistry.java @@ -2,7 +2,7 @@ import java.util.Map; -import jakarta.json.bind.Jsonb; +import com.fasterxml.jackson.databind.ObjectMapper; import io.smallrye.graphql.schema.model.InputType; @@ -21,15 +21,15 @@ private JsonInputRegistry() { } public static void register(InputType inputType) { - JsonBCreator.register(inputType); + JacksonCreator.register(inputType); InputFieldsInfo.register(inputType); } /** - * Override Jsonb config for particular classes by the Jsonb instances + * Override ObjectMapper config for particular classes by the ObjectMapper instances * supplied by the user via an EventingService */ - public static void override(Map overrides) { - JsonBCreator.override(overrides); + public static void override(Map overrides) { + JacksonCreator.override(overrides); } } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java index 756e827a6..e8841b66b 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/GraphQLScalarTypes.java @@ -63,6 +63,8 @@ public static void addObject() { public static void addJson() { SCALAR_MAP.put(Object.class.getName(), ExtendedScalars.Json); + SCALAR_MAP.put("com.fasterxml.jackson.databind.JsonNode", ExtendedScalars.Json); + SCALAR_MAP.put("com.fasterxml.jackson.databind.node.ObjectNode", ExtendedScalars.Json); SCALARS_BY_NAME.put(ExtendedScalars.Json.getName(), ExtendedScalars.Json); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/spi/EventingService.java b/server/implementation/src/main/java/io/smallrye/graphql/spi/EventingService.java index 859986524..dcb67f8f7 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/spi/EventingService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/spi/EventingService.java @@ -3,7 +3,7 @@ import java.util.Collections; import java.util.Map; -import jakarta.json.bind.Jsonb; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.GraphQL; import graphql.schema.GraphQLSchema; @@ -33,7 +33,7 @@ default Operation createOperation(Operation operation) { return operation; } - default Map overrideJsonbConfig() { + default Map overrideObjectMapperConfig() { return Collections.emptyMap(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/websocket/AbstractGraphQLWebsocketHandler.java b/server/implementation/src/main/java/io/smallrye/graphql/websocket/AbstractGraphQLWebsocketHandler.java index d1d14aaee..5aef37965 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/websocket/AbstractGraphQLWebsocketHandler.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/websocket/AbstractGraphQLWebsocketHandler.java @@ -212,7 +212,7 @@ private void sendSingleMessage(String operationId, ExecutionResponse executionRe if (activeOperations.remove(operationId) != null) { session.sendMessage( createDataMessage(operationId, - executionResponse.getExecutionResultAsJsonObject()) + executionResponse.getExecutionResultAsJsonPObject()) .toString()); session.sendMessage(createCompleteMessage(operationId).toString()); } @@ -320,7 +320,7 @@ public void onNext(ExecutionResult executionResult) { try { session.sendMessage( createDataMessage(operationId, - executionResponse.getExecutionResultAsJsonObject()) + executionResponse.getExecutionResultAsJsonPObject()) .toString()); } catch (IOException e) { LOG.warn(e); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java b/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java index 3133583e2..3d236f86f 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqltransportws/GraphQLTransportWSSubprotocolHandler.java @@ -83,7 +83,7 @@ protected void closeDueToConnectionNotInitialized() { @Override protected void sendErrorMessage(String operationId, ExecutionResponse executionResponse) throws IOException { session.sendMessage(createErrorMessage(operationId, - executionResponse.getExecutionResultAsJsonObject().getJsonArray("errors")).toString()); + executionResponse.getExecutionResultAsJsonPObject().getJsonArray("errors")).toString()); } private JsonObject createErrorMessage(String operationId, JsonArray errors) { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqlws/GraphQLWSSubprotocolHandler.java b/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqlws/GraphQLWSSubprotocolHandler.java index 927c42e3f..30f462ec2 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqlws/GraphQLWSSubprotocolHandler.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/websocket/graphqlws/GraphQLWSSubprotocolHandler.java @@ -77,7 +77,7 @@ protected String getPingMessage() { protected void sendErrorMessage(String operationId, ExecutionResponse executionResponse) throws IOException { session.sendMessage(createErrorMessage(operationId, // TODO: the message should have a single error, but executionresult contains an array of errors? what do? - executionResponse.getExecutionResultAsJsonObject().getJsonArray("errors").get(0) + executionResponse.getExecutionResultAsJsonPObject().getJsonArray("errors").get(0) .asJsonObject()) .toString()); } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java index b67765674..2f9db34c6 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java @@ -8,11 +8,11 @@ import java.util.Map; import java.util.Optional; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; - import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import graphql.ExceptionWhileDataFetching; import graphql.GraphQLError; import graphql.GraphqlErrorException; @@ -43,13 +43,13 @@ void testToJsonErrors_WhenExceptionWhileDataFetchingErrorCaught_ShouldReturnJson graphqlErrorException, new SourceLocation(1, 1)); // When - JsonArray jsonArray = executionErrorsService.toJsonErrors(singletonList(exceptionWhileDataFetching)); + ArrayNode jsonArray = executionErrorsService.toJsonErrors(singletonList(exceptionWhileDataFetching)); // Then - JsonObject extensionJsonObject = jsonArray.getJsonObject(0).getJsonObject("extensions"); - assertThat(extensionJsonObject.getString("exception")).isEqualTo("graphql.GraphqlErrorException"); - assertThat(extensionJsonObject.getString("classification")).isEqualTo("DataFetchingException"); - assertThat(extensionJsonObject.getString("code")).isEqualTo("OPERATION_FAILED"); + ObjectNode extensionJsonObject = (ObjectNode) jsonArray.get(0).get("extensions"); + assertThat(extensionJsonObject.get("exception").asText()).isEqualTo("graphql.GraphqlErrorException"); + assertThat(extensionJsonObject.get("classification").asText()).isEqualTo("DataFetchingException"); + assertThat(extensionJsonObject.get("code").asText()).isEqualTo("OPERATION_FAILED"); } @Test @@ -65,15 +65,15 @@ void testToJsonErrors_WhenExceptionWhileValidationErrorCaught_ShouldReturnJsonBo .build(); // When - JsonArray jsonArray = executionErrorsService.toJsonErrors(singletonList(validationError)); + ArrayNode jsonArray = executionErrorsService.toJsonErrors(singletonList(validationError)); // Then - JsonObject extensionJsonObject = jsonArray.getJsonObject(0).getJsonObject("extensions"); - assertThat(extensionJsonObject.getString("description")).isEqualTo("TestDescription"); - assertThat(extensionJsonObject.getString("validationErrorType")).isEqualTo("UnknownDirective"); - assertThat(extensionJsonObject.getJsonArray("queryPath").getString(0)).isEqualTo("Test-Path"); - assertThat(extensionJsonObject.getString("classification")).isEqualTo("ValidationError"); - assertThat(extensionJsonObject.getString("code")).isEqualTo("OPERATION_FAILED"); + ObjectNode extensionJsonObject = (ObjectNode) jsonArray.get(0).get("extensions"); + assertThat(extensionJsonObject.get("description").asText()).isEqualTo("TestDescription"); + assertThat(extensionJsonObject.get("validationErrorType").asText()).isEqualTo("UnknownDirective"); + assertThat(extensionJsonObject.get("queryPath").get(0).asText()).isEqualTo("Test-Path"); + assertThat(extensionJsonObject.get("classification").asText()).isEqualTo("ValidationError"); + assertThat(extensionJsonObject.get("code").asText()).isEqualTo("OPERATION_FAILED"); } @Test @@ -84,14 +84,14 @@ public DummyBusinessException(String message) { } } - JsonArray jsonArray = whenConverting(new DummyBusinessException("dummy-message")); + ArrayNode jsonArray = whenConverting(new DummyBusinessException("dummy-message")); - JsonObject extensions = jsonArray.getJsonObject(0).getJsonObject("extensions"); - assertThat(extensions.getString("exception")).isEqualTo(DummyBusinessException.class.getName()); - assertThat(extensions.getString("code", null)).isEqualTo("dummy-business"); + ObjectNode extensions = (ObjectNode) jsonArray.get(0).get("extensions"); + assertThat(extensions.get("exception").asText()).isEqualTo(DummyBusinessException.class.getName()); + assertThat(extensions.get("code").asText()).isEqualTo("dummy-business"); } - private JsonArray whenConverting(RuntimeException exception) { + private ArrayNode whenConverting(RuntimeException exception) { ResultPath path = ResultPath.parse("/foo/bar"); SourceLocation location = new SourceLocation(12, 34); GraphQLError graphQLError = new GraphQLExceptionWhileDataFetching(path, exception, location); diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientInjectionTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientInjectionTest.java index ef452fc96..16ffade01 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientInjectionTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientInjectionTest.java @@ -9,12 +9,13 @@ import java.util.concurrent.ExecutionException; import jakarta.inject.Inject; -import jakarta.json.JsonObject; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.GraphQLClient; import io.smallrye.graphql.client.core.Document; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; @@ -53,9 +54,9 @@ public void testInjectedClient() throws ExecutionException, InterruptedException field("simple", field("string"), field("integer")))); - JsonObject data = client.executeSync(document).getData(); - assertEquals("asdf", data.getJsonObject("simple").getString("string")); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); + ObjectNode data = client.executeSync(document).getData(); + assertEquals("asdf", ((ObjectNode) data.get("simple")).get("string").asText()); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); } // check that the injected client instance passes the HTTP header that was requested in the configuration diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientSingleOperationsTestBase.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientSingleOperationsTestBase.java index c6fdae64b..aea156eed 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientSingleOperationsTestBase.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/DynamicClientSingleOperationsTestBase.java @@ -17,14 +17,14 @@ import java.util.Map; import java.util.concurrent.ExecutionException; -import jakarta.json.JsonObject; - import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.core.Document; import io.smallrye.graphql.client.core.ScalarType; @@ -50,9 +50,9 @@ public void testOneQueryInOneOperationSync() throws ExecutionException, Interrup field("simple", field("string"), field("integer")))); - JsonObject data = client.executeSync(document).getData(); - assertEquals("asdf", data.getJsonObject("simple").getString("string")); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); + ObjectNode data = client.executeSync(document).getData(); + assertEquals("asdf", ((ObjectNode) data.get("simple")).get("string").asText()); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); } @Test @@ -64,77 +64,78 @@ public void testQueryWithVars() throws ExecutionException, InterruptedException args(arg("number", var)), // the query has a 'number' parameter field("integer")))); // field we want to retrieve Map variableValues = Collections.singletonMap("x", 12345); - JsonObject data = client.executeSync(document, variableValues).getData(); - assertEquals(12345, data.getJsonObject("queryWithArgument").getInt("integer")); + ObjectNode data = client.executeSync(document, variableValues).getData(); + assertEquals(12345, ((ObjectNode) data.get("queryWithArgument")).get("integer").asInt()); } @Test public void testStringQuery() throws ExecutionException, InterruptedException { - JsonObject data = client.executeSync("query {simple{string integer}}").getData(); - assertEquals("asdf", data.getJsonObject("simple").getString("string")); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); + ObjectNode data = client.executeSync("query {simple{string integer}}").getData(); + assertEquals("asdf", ((ObjectNode) data.get("simple")).get("string").asText()); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); } @Test public void testStringQueryWithVars() throws ExecutionException, InterruptedException { Map vars = new HashMap<>(); vars.put("x", 67); - JsonObject data = client + ObjectNode data = client .executeSync("query($x:Int) {queryWithArgument(number: $x){integer}}", vars) .getData(); - assertEquals(67, data.getJsonObject("queryWithArgument").getInt("integer")); + assertEquals(67, ((ObjectNode) data.get("queryWithArgument")).get("integer").asInt()); } @Test public void testStringQueryWithObject() throws ExecutionException, InterruptedException { Map vars = new HashMap<>(); vars.put("x", new DummyObject("a", "b")); - JsonObject data = client + ObjectNode data = client .executeSync("query($x: DummyObjectInput) {queryWithArgument2(obj: $x){dummyObject{a}}}", vars) .getData(); System.out.println(data); - assertEquals("a", data.getJsonObject("queryWithArgument2").getJsonObject("dummyObject").getString("a")); + assertEquals("a", + ((ObjectNode) ((ObjectNode) data.get("queryWithArgument2")).get("dummyObject")).get("a").asText()); } @Test public void testStringQueryWithEnum() throws ExecutionException, InterruptedException { Map vars = new HashMap<>(); vars.put("x", DummyEnum.TWO); - JsonObject data = client + ObjectNode data = client .executeSync("query($x: DummyEnum) {queryWithArgument3(obj: $x){integer}}", vars) .getData(); System.out.println(data); - assertEquals(2, data.getJsonObject("queryWithArgument3").getInt("integer")); + assertEquals(2, ((ObjectNode) data.get("queryWithArgument3")).get("integer").asInt()); } @Test public void testStringQueryWithMultipleOperations() throws ExecutionException, InterruptedException { String query = "query a {simple{integer}} " + "query b {simple2{integer}}"; - JsonObject data = client.executeSync(query, "a").getData(); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); + ObjectNode data = client.executeSync(query, "a").getData(); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); data = client.executeSync(query, "b").getData(); - assertEquals(31, data.getJsonObject("simple2").getInt("integer")); + assertEquals(31, ((ObjectNode) data.get("simple2")).get("integer").asInt()); } @Test public void testStringQueryWithName() throws ExecutionException, InterruptedException { - JsonObject data = client.executeSync("query MyAwesomeQuery {simple{string integer}}").getData(); - assertEquals("asdf", data.getJsonObject("simple").getString("string")); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); + ObjectNode data = client.executeSync("query MyAwesomeQuery {simple{string integer}}").getData(); + assertEquals("asdf", ((ObjectNode) data.get("simple")).get("string").asText()); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); } @Test public void testStringQueryUnspecified() throws ExecutionException, InterruptedException { - JsonObject data = client.executeSync("{simple{string integer}}").getData(); - assertEquals("asdf", data.getJsonObject("simple").getString("string")); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); + ObjectNode data = client.executeSync("{simple{string integer}}").getData(); + assertEquals("asdf", ((ObjectNode) data.get("simple")).get("string").asText()); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); } @Test public void testStringQueryWithArguments() throws ExecutionException, InterruptedException { - JsonObject data = client.executeSync("query {queryWithArgument(number: 20){integer}}").getData(); - assertEquals(20, data.getJsonObject("queryWithArgument").getInt("integer")); + ObjectNode data = client.executeSync("query {queryWithArgument(number: 20){integer}}").getData(); + assertEquals(20, ((ObjectNode) data.get("queryWithArgument")).get("integer").asInt()); } @Test @@ -146,11 +147,11 @@ public void testTwoQueriesInOneOperationSync() throws ExecutionException, Interr field("simple2", field("string"), field("integer")))); - JsonObject data = client.executeSync(document).getData(); - assertEquals("asdf", data.getJsonObject("simple").getString("string")); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); - assertEquals("asdfgh", data.getJsonObject("simple2").getString("string")); - assertEquals(31, data.getJsonObject("simple2").getInt("integer")); + ObjectNode data = client.executeSync(document).getData(); + assertEquals("asdf", ((ObjectNode) data.get("simple")).get("string").asText()); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); + assertEquals("asdfgh", ((ObjectNode) data.get("simple2")).get("string").asText()); + assertEquals(31, ((ObjectNode) data.get("simple2")).get("integer").asInt()); } @Test @@ -159,10 +160,10 @@ public void testSimpleQueryAsync() { field("simple", field("string"), field("integer")))); - JsonObject data = client.executeAsync(document) + ObjectNode data = client.executeAsync(document) .await().atMost(Duration.ofSeconds(30)).getData(); - assertEquals("asdf", data.getJsonObject("simple").getString("string")); - assertEquals(30, data.getJsonObject("simple").getInt("integer")); + assertEquals("asdf", ((ObjectNode) data.get("simple")).get("string").asText()); + assertEquals(30, ((ObjectNode) data.get("simple")).get("integer").asInt()); } @Test @@ -172,8 +173,8 @@ public void testSimpleQueryWithArgument() throws ExecutionException, Interrupted args(arg("number", 12)), field("integer")))); Response response = client.executeSync(document); - JsonObject data = response.getData(); - assertEquals(12, data.getJsonObject("queryWithArgument").getInt("integer")); + ObjectNode data = response.getData(); + assertEquals(12, ((ObjectNode) data.get("queryWithArgument")).get("integer").asInt()); } /** diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/NestedRecordsTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/NestedRecordsTest.java index f1d50c168..74c28215d 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/NestedRecordsTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/NestedRecordsTest.java @@ -9,13 +9,12 @@ import static io.smallrye.graphql.client.core.Operation.operation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeFalse; import java.net.URL; import java.util.List; -import jakarta.json.JsonArray; -import jakarta.json.JsonValue; import jakarta.json.bind.annotation.JsonbCreator; import org.eclipse.microprofile.graphql.GraphQLApi; @@ -29,6 +28,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.core.Document; import io.smallrye.graphql.client.core.InputObject; @@ -62,10 +64,11 @@ public void testNestedRecordWithMissingFieldInQuery() throws Exception { field("needed"), field("notNeeded"))))); Response response = client.executeSync(query); - assertEquals("bla", response.getData().getJsonObject("testParent").getJsonObject("testRecord").getString("needed")); - assertEquals(JsonValue.NULL, - response.getData().getJsonObject("testParent").getJsonObject("testRecord").get("notNeeded")); - assertNull(response.getData().getJsonObject("testParent").get("s")); + ObjectNode testParent = (ObjectNode) response.getData().get("testParent"); + ObjectNode testRecord = (ObjectNode) testParent.get("testRecord"); + assertEquals("bla", testRecord.get("needed").asText()); + assertTrue(testRecord.get("notNeeded").isNull()); + assertNull(testParent.get("s")); } } @@ -90,12 +93,13 @@ public void testNestedRecordWithListWithMissingFieldInQuery() throws Exception { field("needed"), field("notNeeded"))))); Response response = client.executeSync(query); - JsonArray echoedRecords = response.getData().getJsonObject("testParentWithList").getJsonArray("testRecords"); + ArrayNode echoedRecords = (ArrayNode) ((ObjectNode) response.getData().get("testParentWithList")) + .get("testRecords"); - assertEquals("bla", echoedRecords.get(0).asJsonObject().getString("needed")); - assertEquals(JsonValue.NULL, echoedRecords.get(0).asJsonObject().get("notNeeded")); - assertEquals("bla2", echoedRecords.get(1).asJsonObject().getString("needed")); - assertEquals(JsonValue.NULL, echoedRecords.get(1).asJsonObject().get("notNeeded")); + assertEquals("bla", ((ObjectNode) echoedRecords.get(0)).get("needed").asText()); + assertTrue(((ObjectNode) echoedRecords.get(0)).get("notNeeded").isNull()); + assertEquals("bla2", ((ObjectNode) echoedRecords.get(1)).get("needed").asText()); + assertTrue(((ObjectNode) echoedRecords.get(1)).get("notNeeded").isNull()); } } @@ -116,12 +120,13 @@ public void testNestedRecordWithArrayWithMissingFieldInQuery() throws Exception field("needed"), field("notNeeded"))))); Response response = client.executeSync(query); - JsonArray echoedRecords = response.getData().getJsonObject("testParentWithArray").getJsonArray("testRecords"); + ArrayNode echoedRecords = (ArrayNode) ((ObjectNode) response.getData().get("testParentWithArray")) + .get("testRecords"); - assertEquals("bla", echoedRecords.get(0).asJsonObject().getString("needed")); - assertEquals(JsonValue.NULL, echoedRecords.get(0).asJsonObject().get("notNeeded")); - assertEquals("bla2", echoedRecords.get(1).asJsonObject().getString("needed")); - assertEquals(JsonValue.NULL, echoedRecords.get(1).asJsonObject().get("notNeeded")); + assertEquals("bla", ((ObjectNode) echoedRecords.get(0)).get("needed").asText()); + assertTrue(((ObjectNode) echoedRecords.get(0)).get("notNeeded").isNull()); + assertEquals("bla2", ((ObjectNode) echoedRecords.get(1)).get("needed").asText()); + assertTrue(((ObjectNode) echoedRecords.get(1)).get("notNeeded").isNull()); } } @@ -137,8 +142,9 @@ public void testSimpleRecordWithMissingFieldInQuery() throws Exception { field("needed"), field("notNeeded")))); Response response = client.executeSync(query); - assertEquals("bla", response.getData().getJsonObject("echo").getString("needed")); - assertEquals(JsonValue.NULL, response.getData().getJsonObject("echo").get("notNeeded")); + ObjectNode echo = (ObjectNode) response.getData().get("echo"); + assertEquals("bla", echo.get("needed").asText()); + assertTrue(echo.get("notNeeded").isNull()); } } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/RecordTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/RecordTest.java index 0113264c7..3213fbc25 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/RecordTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/RecordTest.java @@ -12,8 +12,6 @@ import java.net.URL; import java.util.Set; -import jakarta.json.bind.annotation.JsonbCreator; - import org.eclipse.microprofile.graphql.GraphQLApi; import org.eclipse.microprofile.graphql.NonNull; import org.eclipse.microprofile.graphql.Query; @@ -26,6 +24,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.core.Document; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; @@ -58,8 +61,9 @@ public void testSimpleRecord() throws Exception { field("a"), field("b")))); Response response = client.executeSync(query); - assertEquals("a", response.getData().getJsonObject("simple").getString("a")); - assertEquals("b", response.getData().getJsonObject("simple").getString("b")); + ObjectNode simple = (ObjectNode) response.getData().get("simple"); + assertEquals("a", simple.get("a").asText()); + assertEquals("b", simple.get("b").asText()); } } @@ -81,12 +85,15 @@ public void testSimpleRecordWithFactory() throws Exception { Response response = client.executeSync(query); System.out.println(response); System.out.println("query.build() = " + query.build()); - assertEquals("a", response.getData().getJsonObject("simpleWithFactory").getString("a")); - assertEquals("b", response.getData().getJsonObject("simpleWithFactory").getString("b")); - assertEquals("c", response.getData().getJsonObject("simpleWithFactory").getJsonArray("c").getString(0)); - assertEquals("cc", response.getData().getJsonObject("simpleWithFactory").getJsonArray("c").getString(1)); - assertEquals("dd", response.getData().getJsonObject("simpleWithFactory").getJsonArray("d").getString(0)); - assertEquals("d", response.getData().getJsonObject("simpleWithFactory").getJsonArray("d").getString(1)); + ObjectNode simpleWithFactory = (ObjectNode) response.getData().get("simpleWithFactory"); + assertEquals("a", simpleWithFactory.get("a").asText()); + assertEquals("b", simpleWithFactory.get("b").asText()); + ArrayNode c = (ArrayNode) simpleWithFactory.get("c"); + assertEquals("c", c.get(0).asText()); + assertEquals("cc", c.get(1).asText()); + ArrayNode d = (ArrayNode) simpleWithFactory.get("d"); + assertEquals("dd", d.get(0).asText()); + assertEquals("d", d.get(1).asText()); } } @@ -103,8 +110,9 @@ public void testSimpleRecordWithParameterizedConstructor() throws Exception { Response response = client.executeSync(query); System.out.println(response); System.out.println("query.build() = " + query.build()); - assertEquals(1, response.getData().getJsonObject("simpleWithParameterizedConstructor").getInt("a")); - assertEquals("b", response.getData().getJsonObject("simpleWithParameterizedConstructor").getString("b")); + ObjectNode result = (ObjectNode) response.getData().get("simpleWithParameterizedConstructor"); + assertEquals(1, result.get("a").asInt()); + assertEquals("b", result.get("b").asText()); } } @@ -135,7 +143,6 @@ public record SimpleRecord(String a, String b) { public record SimpleRecordWithFactory(String a, String b, String[] c, Set d) { - @JsonbCreator public static SimpleRecordWithFactory build(String a, String b, String[] c, Set d) { return new SimpleRecordWithFactory(a, b, c, d); } @@ -144,8 +151,8 @@ public static SimpleRecordWithFactory build(String a, String b, String[] c, Set< public record SimpleRecordWithParameterizedConstructor(@NonNull Integer a, @NonNull String b) { - @JsonbCreator - public SimpleRecordWithParameterizedConstructor(String b) { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public SimpleRecordWithParameterizedConstructor(@JsonProperty("b") String b) { this(1, b); } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientInboundExtensionsTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientInboundExtensionsTest.java index 0ec33a266..0850de36c 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientInboundExtensionsTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientInboundExtensionsTest.java @@ -12,8 +12,6 @@ import java.util.concurrent.ExecutionException; import jakarta.inject.Inject; -import jakarta.json.Json; -import jakarta.json.JsonObject; import org.eclipse.microprofile.graphql.GraphQLApi; import org.eclipse.microprofile.graphql.Query; @@ -27,6 +25,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClient; import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; @@ -66,11 +67,15 @@ public void emptyExtensionsTest() throws ExecutionException, InterruptedExceptio } @Test - public void addedExtensionsTest() throws ExecutionException, InterruptedException { + public void addedExtensionsTest() throws Exception { Response response = client.executeSync(document(operation( field("poolWithExtensions", field("volume"))))); - assertEquals(getJsonMap(), response.getExtensions()); + // Compare structurally by converting both to JsonNode through a common JSON round-trip + ObjectMapper mapper = new ObjectMapper(); + JsonNode expected = mapper.readTree(mapper.writeValueAsString(getMap())); + JsonNode actual = mapper.readTree(response.getExtensions().toString()); + assertEquals(expected, actual); } @GraphQLApi @@ -123,16 +128,4 @@ private static Map getMap() { return map; } - private JsonObject getJsonMap() { - JsonObject result = Json.createObjectBuilder() - .add("int", 23) - .add("double", 3.1415926535) - .add("list of strings", Json.createArrayBuilder().add("1").add("2").add("3").add("4").build()) - .add("subMap", Json.createObjectBuilder() - .add("1", "Paul") - .add("3", "John") - .build()) - .build(); - return result; - } } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientOutboundExtensionsTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientOutboundExtensionsTest.java index 2250f9526..af2608775 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientOutboundExtensionsTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientOutboundExtensionsTest.java @@ -64,7 +64,7 @@ public void sendExtensionsToServerTest() throws ExecutionException, InterruptedE .build(); Response response = client.executeSync(request); - assertEquals("myValue", response.getData().getString("echoExtension")); + assertEquals("myValue", response.getData().get("echoExtension").asText()); } @Test @@ -72,7 +72,7 @@ public void sendNoExtensionsToServerTest() throws ExecutionException, Interrupte Request request = RequestImpl.builder("{ echoExtension(key: \"myKey\") }").build(); Response response = client.executeSync(request); - assertEquals("null", response.getData().getString("echoExtension")); + assertEquals("null", response.getData().get("echoExtension").asText()); } @GraphQLApi diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/fragments/DynamicClientFragmentTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/fragments/DynamicClientFragmentTest.java index dd7c4bf6c..5a044fcde 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/fragments/DynamicClientFragmentTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/fragments/DynamicClientFragmentTest.java @@ -12,9 +12,6 @@ import java.net.URL; import java.util.concurrent.ExecutionException; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; - import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.RunAsClient; import org.jboss.arquillian.junit5.ArquillianExtension; @@ -27,6 +24,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.core.Document; import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClient; @@ -90,16 +90,16 @@ private void validateResponse(Response response) { Assertions.fail("Error was returned: " + response.getErrors()); } - JsonArray vehicles = response.getData().getJsonArray("vehicles"); + ArrayNode vehicles = (ArrayNode) response.getData().get("vehicles"); - JsonObject car = vehicles.getJsonObject(0); - assertEquals(4, car.getInt("wheelsCount")); - assertEquals(8, car.getInt("engineCylinders")); + ObjectNode car = (ObjectNode) vehicles.get(0); + assertEquals(4, car.get("wheelsCount").asInt()); + assertEquals(8, car.get("engineCylinders").asInt()); assertNull(car.get("frameSize")); - JsonObject bicycle = vehicles.getJsonObject(1); - assertEquals(2, bicycle.getInt("wheelsCount")); - assertEquals(15, bicycle.getInt("frameSize")); + ObjectNode bicycle = (ObjectNode) vehicles.get(1); + assertEquals(2, bicycle.get("wheelsCount").asInt()); + assertEquals(15, bicycle.get("frameSize").asInt()); assertNull(bicycle.get("engineCylinders")); } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/subscription/AbstractDynamicClientSubscriptionTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/subscription/AbstractDynamicClientSubscriptionTest.java index e10198dbc..0ca540d89 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/subscription/AbstractDynamicClientSubscriptionTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/subscription/AbstractDynamicClientSubscriptionTest.java @@ -56,7 +56,7 @@ public void testCounting() { .collect(Collectors.toList()); for (int i = 0; i < 5; i++) { Response response = responses.get(i); - assertEquals(i, response.getData().getInt("countToFive")); + assertEquals(i, response.getData().get("countToFive").asInt()); assertNoErrors(response.getErrors()); } } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/parsing/DynamicClientFormatAnnotationsTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/parsing/DynamicClientFormatAnnotationsTest.java index 9d08df6a6..4b94d4a28 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/parsing/DynamicClientFormatAnnotationsTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/parsing/DynamicClientFormatAnnotationsTest.java @@ -66,7 +66,7 @@ public void testParsingDateWithCustomFormat() throws ExecutionException, Interru // a sanity check to make sure that the server really returned the custom format // because if not, then this test would not be actually verifying anything assertEquals("May 1997 13 04,20,03 May Tue", - response.getData().getJsonObject("something").getString("date"), + response.getData().get("something").get("date").asText(), "Sanity check failed: the server did not return the date in the desired custom format"); ObjectWithFormattedFields objectWithFormattedFields = response.getObject(ObjectWithFormattedFields.class, "something"); @@ -105,7 +105,7 @@ public void testParsingDoubleWithCustomFormat() throws ExecutionException, Inter field("doubleNumber")))); Response response = client.executeSync(document); - assertEquals("12.345.678,9", response.getData().getJsonObject("something").getString("doubleNumber"), + assertEquals("12.345.678,9", response.getData().get("something").get("doubleNumber").asText(), "Sanity check failed: the server did not return the number in the desired custom format"); ObjectWithFormattedFields objectWithFormattedFields = response.getObject(ObjectWithFormattedFields.class, "something"); diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/voidmutation/TypesafeVoidMutationTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/voidmutation/TypesafeVoidMutationTest.java index 1829f5663..615c2b4bf 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/voidmutation/TypesafeVoidMutationTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/voidmutation/TypesafeVoidMutationTest.java @@ -7,8 +7,6 @@ import java.net.URL; -import jakarta.json.Json; - import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.RunAsClient; import org.jboss.arquillian.junit5.ArquillianExtension; @@ -94,9 +92,7 @@ public void typeSafeResponseVoidMutationTest() { assertNull(result.get()); assertThat(client.findAllRectangles().size()).isEqualTo(4); assertThat(client.findAllRectangles()).contains(rectangle); - assertThat(result.getExtensions()).isEqualTo(Json.createObjectBuilder() - .add("pi", 3.1415) - .build()); + assertThat(result.getExtensions().toString()).isEqualTo("{\"pi\":3.1415}"); } @Test diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/json/CustomJsonbService.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/json/CustomJsonbService.java index 1fe62f1f9..54ee640c9 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/json/CustomJsonbService.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/json/CustomJsonbService.java @@ -1,11 +1,10 @@ package io.smallrye.graphql.tests.json; +import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Map; -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; +import com.fasterxml.jackson.databind.ObjectMapper; import io.smallrye.graphql.spi.EventingService; @@ -17,8 +16,9 @@ public String getConfigKey() { } @Override - public Map overrideJsonbConfig() { - JsonbConfig config = new JsonbConfig().withDateFormat("MM dd yyyy HH:mm Z", null); - return Collections.singletonMap(DateWrapper.class.getName(), JsonbBuilder.create(config)); + public Map overrideObjectMapperConfig() { + ObjectMapper mapper = new ObjectMapper(); + mapper.setDateFormat(new SimpleDateFormat("MM dd yyyy HH:mm Z")); + return Collections.singletonMap(DateWrapper.class.getName(), mapper); } } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/subscription/SubscriptionFieldBatchingTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/subscription/SubscriptionFieldBatchingTest.java index ac3ea249c..72681d8db 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/subscription/SubscriptionFieldBatchingTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/subscription/SubscriptionFieldBatchingTest.java @@ -6,9 +6,6 @@ import java.util.List; import java.util.stream.Collectors; -import jakarta.json.Json; -import jakarta.json.JsonObject; - import org.eclipse.microprofile.graphql.GraphQLApi; import org.eclipse.microprofile.graphql.Source; import org.jboss.arquillian.container.test.api.Deployment; @@ -19,6 +16,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.smallrye.graphql.api.Subscription; import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; @@ -105,16 +104,16 @@ public void subscriptionFieldBatchingTest() throws Exception { .isNotNull(); assertThat(response.getErrors()).isNull(); - JsonObject content = response.getData().get("subscriptionOperation").asJsonObject(); + ObjectNode content = (ObjectNode) response.getData().get("subscriptionOperation"); assertThat(content).isNotNull(); - assertThat(content.containsKey("field1")).isTrue(); - assertThat(content.containsKey("field2")).isTrue(); - assertThat(content.containsKey("sourceBatchField")).isTrue(); + assertThat(content.has("field1")).isTrue(); + assertThat(content.has("field2")).isTrue(); + assertThat(content.has("sourceBatchField")).isTrue(); - assertThat(content.get("field1")).isEqualTo(Json.createValue(111 * (i + 1))); - assertThat(content.get("field2")).isEqualTo(Json.createValue("bar" + (i + 1))); - assertThat(content.get("sourceBatchField")).isEqualTo(Json.createValue(i + 1)); + assertThat(content.get("field1").asInt()).isEqualTo(111 * (i + 1)); + assertThat(content.get("field2").asText()).isEqualTo("bar" + (i + 1)); + assertThat(content.get("sourceBatchField").asInt()).isEqualTo(i + 1); } } }