diff --git a/akka-javasdk/src/main/java/akka/javasdk/JsonSupport.java b/akka-javasdk/src/main/java/akka/javasdk/JsonSupport.java index 42951632c..94f10bfaa 100644 --- a/akka-javasdk/src/main/java/akka/javasdk/JsonSupport.java +++ b/akka-javasdk/src/main/java/akka/javasdk/JsonSupport.java @@ -5,10 +5,10 @@ package akka.javasdk; import akka.Done; -import akka.annotation.InternalApi; import akka.javasdk.annotations.Migration; import akka.javasdk.impl.AnySupport; import akka.javasdk.impl.ByteStringEncoding; +import akka.runtime.sdk.spi.BytesPayload; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -82,6 +82,8 @@ public static ObjectMapper getObjectMapper() { return objectMapper; } + private static akka.javasdk.impl.serialization.JsonSerializer jsonSerializer = new akka.javasdk.impl.serialization.JsonSerializer(); + private JsonSupport() { } @@ -96,7 +98,10 @@ private JsonSupport() { * for the JSON type instead. * * @see {{encodeJson(T, String}} + * + * @deprecated Protobuf Any with JSON is not supported */ + @Deprecated public static Any encodeJson(T value) { return encodeJson(value, value.getClass().getName()); } @@ -111,7 +116,10 @@ public static Any encodeJson(T value) { * JSON, useful for example when multiple different objects are passed through a pub/sub * topic. * @throws IllegalArgumentException if the given value cannot be turned into JSON + * + * @deprecated Protobuf Any with JSON is not supported */ + @Deprecated public static Any encodeJson(T value, String jsonType) { try { ByteString bytes = encodeToBytes(value); @@ -123,26 +131,89 @@ public static Any encodeJson(T value, String jsonType) { } } - // FIXME do we really want all these to be public API? + /** + * @deprecated Use encodeToAkkaByteString + */ + @Deprecated public static ByteString encodeToBytes(T value) throws JsonProcessingException { return UnsafeByteOperations.unsafeWrap( objectMapper.writerFor(value.getClass()).writeValueAsBytes(value)); } - public static akka.util.ByteString encodeToAkkaByteString(T value) throws JsonProcessingException { - return akka.util.ByteString.fromArrayUnsafe(objectMapper.writerFor(value.getClass()).writeValueAsBytes(value)); + /** + * Encode the given value as JSON using Jackson. + * + * @param value the object to encode as JSON, must be an instance of a class properly annotated + * with the needed Jackson annotations. + * @throws IllegalArgumentException if the given value cannot be turned into JSON + */ + public static akka.util.ByteString encodeToAkkaByteString(T value) { + try { + return akka.util.ByteString.fromArrayUnsafe(objectMapper.writerFor(value.getClass()).writeValueAsBytes(value)); + } catch (JsonProcessingException ex) { + throw new IllegalArgumentException( + "Could not encode [" + value.getClass().getName() + "] as JSON", ex); + } } - public static akka.util.ByteString encodeDynamicToAkkaByteString(String key, String value) throws JsonProcessingException { - ObjectNode dynamicJson = objectMapper.createObjectNode().put(key, value); - return akka.util.ByteString.fromArrayUnsafe(objectMapper.writeValueAsBytes(dynamicJson)); + /** + * @deprecated was only intended for internal use + */ + @Deprecated + public static akka.util.ByteString encodeDynamicToAkkaByteString(String key, String value) { + try { + ObjectNode dynamicJson = objectMapper.createObjectNode().put(key, value); + return akka.util.ByteString.fromArrayUnsafe(objectMapper.writeValueAsBytes(dynamicJson)); + } catch (JsonProcessingException ex) { + throw new IllegalArgumentException( + "Could not encode dynamic key/value as JSON", ex); + } } - public static akka.util.ByteString encodeDynamicCollectionToAkkaByteString(String key, Collection values) throws JsonProcessingException { - ObjectNode objectNode = objectMapper.createObjectNode(); - ArrayNode dynamicJson = objectNode.putArray(key); - values.forEach(v -> dynamicJson.add(v.toString())); - return akka.util.ByteString.fromArrayUnsafe(objectMapper.writeValueAsBytes(objectNode)); + /** + * @deprecated was only intended for internal use + */ + @Deprecated + public static akka.util.ByteString encodeDynamicCollectionToAkkaByteString(String key, Collection values) { + try { + ObjectNode objectNode = objectMapper.createObjectNode(); + ArrayNode dynamicJson = objectNode.putArray(key); + values.forEach(v -> dynamicJson.add(v.toString())); + return akka.util.ByteString.fromArrayUnsafe(objectMapper.writeValueAsBytes(objectNode)); + } catch (JsonProcessingException ex) { + throw new IllegalArgumentException( + "Could not encode dynamic key/values as JSON", ex); + } + } + + /** + * Decode the given bytes to an instance of T using Jackson. The bytes must be + * the JSON string as bytes. + * + * @param valueClass The type of class to deserialize the object to, the class must have the + * proper Jackson annotations for deserialization. + * @param bytes The bytes to deserialize. + * @return The decoded object + * @throws IllegalArgumentException if the given value cannot be decoded to a T + * + */ + public static T decodeJson(Class valueClass, akka.util.ByteString bytes) { + return jsonSerializer.fromBytes(valueClass, new BytesPayload(bytes, jsonSerializer.contentTypeFor(valueClass))); + } + + /** + * Decode the given bytes to an instance of T using Jackson. The bytes must be + * the JSON string as bytes. + * + * @param valueClass The type of class to deserialize the object to, the class must have the + * proper Jackson annotations for deserialization. + * @param bytes The bytes to deserialize. + * @return The decoded object + * @throws IllegalArgumentException if the given value cannot be decoded to a T + * + */ + public static T decodeJson(Class valueClass, byte[] bytes) { + return decodeJson(valueClass, akka.util.ByteString.fromArrayUnsafe(bytes)); } /** @@ -154,7 +225,10 @@ public static akka.util.ByteString encodeDynamicCollectionToAkkaByteString(Strin * @param any The protobuf Any object to deserialize. * @return The decoded object * @throws IllegalArgumentException if the given value cannot be decoded to a T + * + * @deprecated Protobuf Any with JSON is not supported */ + @Deprecated public static T decodeJson(Class valueClass, Any any) { if (!AnySupport.isJsonTypeUrl(any.getTypeUrl())) { throw new IllegalArgumentException( @@ -177,7 +251,7 @@ public static T decodeJson(Class valueClass, Any any) { if (fromVersion < currentVersion) { return migrate(valueClass, decodedBytes, fromVersion, migration); } else if (fromVersion == currentVersion) { - return parseBytes(decodedBytes.toByteArray(), valueClass); + return objectMapper.readValue(decodedBytes.toByteArray(), valueClass); } else if (fromVersion <= supportedForwardVersion) { return migrate(valueClass, decodedBytes, fromVersion, migration); } else { @@ -185,7 +259,7 @@ public static T decodeJson(Class valueClass, Any any) { "behind version " + fromVersion + " of deserialized type [" + valueClass.getName() + "]"); } } else { - return parseBytes(decodedBytes.toByteArray(), valueClass); + return objectMapper.readValue(decodedBytes.toByteArray(), valueClass); } } catch (JsonProcessingException e) { throw jsonProcessingException(valueClass, any, e); @@ -196,6 +270,10 @@ public static T decodeJson(Class valueClass, Any any) { } } + /** + * @deprecated Use decodeJson + */ + @Deprecated public static T parseBytes(byte[] bytes, Class valueClass) throws IOException { return objectMapper.readValue(bytes, valueClass); } @@ -236,6 +314,11 @@ private static int parseVersion(String typeUrl) { } } + + /** + * @deprecated Protobuf Any with JSON is not supported + */ + @Deprecated public static > C decodeJsonCollection(Class valueClass, Class collectionType, Any any) { if (!AnySupport.isJsonTypeUrl(any.getTypeUrl())) { throw new IllegalArgumentException( @@ -257,6 +340,14 @@ public static > C decodeJsonCollection(Class value } } + public static > C decodeJsonCollection(Class valueClass, Class collectionType, akka.util.ByteString bytes) { + return jsonSerializer.fromBytes(valueClass, collectionType, new BytesPayload(bytes, jsonSerializer.contentTypeFor(valueClass))); + } + + public static > C decodeJsonCollection(Class valueClass, Class collectionType, byte[] bytes) { + return decodeJsonCollection(valueClass, collectionType, akka.util.ByteString.fromArrayUnsafe(bytes)); + } + /** * Decode the given protobuf Any to an instance of T using Jackson but only if the suffix of the * type URL matches the given jsonType. @@ -264,7 +355,10 @@ public static > C decodeJsonCollection(Class value * @return An Optional containing the successfully decoded value or an empty Optional if the type * suffix does not match. * @throws IllegalArgumentException if the suffix matches but the Any cannot be parsed into a T + * + * @deprecated Protobuf Any with JSON is not supported */ + @Deprecated public static Optional decodeJson(Class valueClass, String jsonType, Any any) { if (any.getTypeUrl().endsWith(jsonType)) { return Optional.of(decodeJson(valueClass, any)); @@ -273,15 +367,6 @@ public static Optional decodeJson(Class valueClass, String jsonType, A } } - /** - * INTERNAL API - * @hidden - */ - @InternalApi - public static T decodeJson(Class valueClass, com.google.protobuf.any.Any scalaPbAny) { - var javaAny = com.google.protobuf.any.Any.toJavaProto(scalaPbAny); - return JsonSupport.decodeJson(valueClass, javaAny); - } } class DoneSerializer extends JsonSerializer { diff --git a/akka-javasdk/src/main/java/akka/javasdk/http/HttpResponses.java b/akka-javasdk/src/main/java/akka/javasdk/http/HttpResponses.java index b032a90a2..c9e24378a 100644 --- a/akka-javasdk/src/main/java/akka/javasdk/http/HttpResponses.java +++ b/akka-javasdk/src/main/java/akka/javasdk/http/HttpResponses.java @@ -60,12 +60,8 @@ public static HttpResponse ok(String text) { */ public static HttpResponse ok(Object object) { if (object == null) throw new IllegalArgumentException("object must not be null"); - try { - byte[] body = JsonSupport.encodeToBytes(object).toByteArray(); - return HttpResponse.create().withEntity(ContentTypes.APPLICATION_JSON, body); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + var body = JsonSupport.encodeToAkkaByteString(object); + return HttpResponse.create().withEntity(ContentTypes.APPLICATION_JSON, body); } /** diff --git a/akka-javasdk/src/main/scala/akka/javasdk/impl/client/ViewClientImpl.scala b/akka-javasdk/src/main/scala/akka/javasdk/impl/client/ViewClientImpl.scala index 4e0bc9e2c..e0933d02a 100644 --- a/akka-javasdk/src/main/scala/akka/javasdk/impl/client/ViewClientImpl.scala +++ b/akka-javasdk/src/main/scala/akka/javasdk/impl/client/ViewClientImpl.scala @@ -6,7 +6,6 @@ package akka.javasdk.impl.client import akka.annotation.InternalApi import akka.japi.function -import akka.javasdk.JsonSupport import akka.javasdk.Metadata import akka.javasdk.client.ComponentInvokeOnlyMethodRef import akka.javasdk.client.ComponentInvokeOnlyMethodRef1 @@ -116,12 +115,10 @@ private[javasdk] final case class ViewClientImpl( case Some(arg) => // Note: not Kalix JSON encoded here, regular/normal utf8 bytes if (arg.getClass.isPrimitive || primitiveObjects.contains(arg.getClass)) { - // FIXME eh?, move this to JsonSerializer - val bytes = JsonSupport.encodeDynamicToAkkaByteString(method.getParameters.head.getName, arg.toString) + val bytes = serializer.encodeDynamicToAkkaByteString(method.getParameters.head.getName, arg.toString) new BytesPayload(bytes, JsonSerializer.JsonContentTypePrefix + "object") } else if (classOf[java.util.Collection[_]].isAssignableFrom(arg.getClass)) { - // FIXME eh?, move this to JsonSerializer - val bytes = JsonSupport.encodeDynamicCollectionToAkkaByteString( + val bytes = serializer.encodeDynamicCollectionToAkkaByteString( method.getParameters.head.getName, arg.asInstanceOf[java.util.Collection[_]]) new BytesPayload(bytes, JsonSerializer.JsonContentTypePrefix + "object") diff --git a/akka-javasdk/src/main/scala/akka/javasdk/impl/http/HttpClientImpl.scala b/akka-javasdk/src/main/scala/akka/javasdk/impl/http/HttpClientImpl.scala index 2aece370b..d1320d57d 100644 --- a/akka-javasdk/src/main/scala/akka/javasdk/impl/http/HttpClientImpl.scala +++ b/akka-javasdk/src/main/scala/akka/javasdk/impl/http/HttpClientImpl.scala @@ -117,7 +117,7 @@ private[akka] final case class RequestBuilderImpl[R]( override def withRequestBody(`object`: AnyRef): RequestBuilder[R] = { if (`object` eq null) throw new IllegalArgumentException("object must not be null") try { - val body = JsonSupport.encodeToBytes(`object`).toByteArray + val body = JsonSupport.encodeToAkkaByteString(`object`) val requestWithBody = request.withEntity(ContentTypes.APPLICATION_JSON, body) withRequest(requestWithBody) } catch { @@ -167,7 +167,7 @@ private[akka] final case class RequestBuilderImpl[R]( throw new RuntimeException(errorString + ": " + bytes.utf8String) } } else if (res.entity.getContentType == ContentTypes.APPLICATION_JSON) - new StrictResponse[T](res, JsonSupport.parseBytes(bytes.toArrayUnsafe(), `type`)) + new StrictResponse[T](res, JsonSupport.decodeJson(`type`, bytes)) else if (!res.entity.getContentType.binary && (`type` eq classOf[String])) new StrictResponse[T]( res, diff --git a/akka-javasdk/src/main/scala/akka/javasdk/impl/serialization/JsonSerializer.scala b/akka-javasdk/src/main/scala/akka/javasdk/impl/serialization/JsonSerializer.scala index 31cba9cab..c406b8b39 100644 --- a/akka-javasdk/src/main/scala/akka/javasdk/impl/serialization/JsonSerializer.scala +++ b/akka-javasdk/src/main/scala/akka/javasdk/impl/serialization/JsonSerializer.scala @@ -267,4 +267,25 @@ class JsonSerializer { private[akka] def removeVersion(typeName: String) = { typeName.split("#").head } + + private[akka] def encodeDynamicToAkkaByteString(key: String, value: String): ByteString = { + try { + val dynamicJson = objectMapper.createObjectNode.put(key, value) + ByteString.fromArrayUnsafe(objectMapper.writeValueAsBytes(dynamicJson)) + } catch { + case ex: JsonProcessingException => + throw new IllegalArgumentException("Could not encode dynamic key/value as JSON", ex) + } + } + + private[akka] def encodeDynamicCollectionToAkkaByteString(key: String, values: java.util.Collection[_]): ByteString = + try { + val objectNode = objectMapper.createObjectNode + val dynamicJson = objectNode.putArray(key) + values.forEach(v => dynamicJson.add(v.toString)) + ByteString.fromArrayUnsafe(objectMapper.writeValueAsBytes(objectNode)) + } catch { + case ex: JsonProcessingException => + throw new IllegalArgumentException("Could not encode dynamic key/values as JSON", ex) + } } diff --git a/akka-javasdk/src/test/scala/akka/javasdk/JsonSupportSpec.scala b/akka-javasdk/src/test/scala/akka/javasdk/JsonSupportSpec.scala index 7235bb4c6..1153b2b5d 100644 --- a/akka-javasdk/src/test/scala/akka/javasdk/JsonSupportSpec.scala +++ b/akka-javasdk/src/test/scala/akka/javasdk/JsonSupportSpec.scala @@ -19,8 +19,8 @@ class MyJsonable { @BeanProperty var field: String = _ } +@Deprecated class JsonSupportSpec extends AnyWordSpec with Matchers { - // FIXME move these tests to JsonSerializerSpec val myJsonable = new MyJsonable myJsonable.field = "foo" diff --git a/samples/doc-snippets/src/main/java/com/example/api/ExampleEndpoint.java b/samples/doc-snippets/src/main/java/com/example/api/ExampleEndpoint.java index 32eff24f3..68e16bd55 100644 --- a/samples/doc-snippets/src/main/java/com/example/api/ExampleEndpoint.java +++ b/samples/doc-snippets/src/main/java/com/example/api/ExampleEndpoint.java @@ -114,13 +114,9 @@ public HttpResponse lowerLevelResponseHello(String name, int age) { .withStatus(StatusCodes.BAD_REQUEST) .withEntity("It is unlikely that you are " + age + " years old"); else { - try { var jsonBytes = JsonSupport.encodeToAkkaByteString(new HelloResponse("Hello " + name + "!")); // <1> return HttpResponse.create() // <2> .withEntity(ContentTypes.APPLICATION_JSON, jsonBytes); // <3> - } catch (JsonProcessingException e) { - throw new RuntimeException("Could not serialize response to JSON", e); - } } } // end::even-lower-level-response[] diff --git a/samples/event-sourced-counter-brokers/src/it/java/counter/application/CounterWithRealKafkaIntegrationTest.java b/samples/event-sourced-counter-brokers/src/it/java/counter/application/CounterWithRealKafkaIntegrationTest.java index a09f3e13e..9feee3db1 100644 --- a/samples/event-sourced-counter-brokers/src/it/java/counter/application/CounterWithRealKafkaIntegrationTest.java +++ b/samples/event-sourced-counter-brokers/src/it/java/counter/application/CounterWithRealKafkaIntegrationTest.java @@ -55,7 +55,7 @@ public void verifyCounterEventSourcedConsumesFromKafka() { ConsumerRecords records = consumer.poll(Duration.ofMillis(200)); var foundRecord = false; for (ConsumerRecord r : records) { - var increased = JsonSupport.parseBytes(r.value(), CounterEvent.ValueIncreased.class); + var increased = JsonSupport.decodeJson(CounterEvent.ValueIncreased.class, r.value()); String subjectId = new String(r.headers().headers("ce-subject").iterator().next().value(), StandardCharsets.UTF_8); if (subjectId.equals(counterId) && increased.value() == 20) { foundRecord = true; diff --git a/samples/event-sourced-customer-registry/src/test/java/customer/domain/CustomerEventSerializationTest.java b/samples/event-sourced-customer-registry/src/test/java/customer/domain/CustomerEventSerializationTest.java index 2f4db520d..3f13cf459 100644 --- a/samples/event-sourced-customer-registry/src/test/java/customer/domain/CustomerEventSerializationTest.java +++ b/samples/event-sourced-customer-registry/src/test/java/customer/domain/CustomerEventSerializationTest.java @@ -73,4 +73,4 @@ public void shouldDeserializeCustomerCreated_V0() throws InvalidProtocolBufferEx } // end::testing-deserialization[] -} \ No newline at end of file +}