From 66fad0f4a382687cb4aaa2145d4ed33b73a1576e Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 17 Oct 2025 21:04:01 +0100 Subject: [PATCH] support jackson3 docs javafmt jackson2 in docs Update build.sbt fix tests by using jackson 2 style config Update json-support.md Update json-support.md test fix Update json-support.md Update Jackson.java Update json-support.md revert format change --- build.sbt | 11 ++ docs/src/main/paradox/common/json-support.md | 21 +++ docs/src/main/paradox/introduction.md | 6 +- .../routing-dsl/source-streaming-support.md | 3 +- .../javadsl/marshallers/jackson3/Jackson.java | 150 ++++++++++++++++ .../src/main/resources/reference.conf | 40 +++++ .../marshallers/jackson3/JacksonTest.java | 167 ++++++++++++++++++ project/Dependencies.scala | 6 + 8 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 http-marshallers-java/http-jackson3/src/main/java/org/apache/pekko/http/javadsl/marshallers/jackson3/Jackson.java create mode 100644 http-marshallers-java/http-jackson3/src/main/resources/reference.conf create mode 100644 http-marshallers-java/http-jackson3/src/test/java/org/apache/pekko/http/javadsl/marshallers/jackson3/JacksonTest.java diff --git a/build.sbt b/build.sbt index 44dd60fd19..2c7127f584 100644 --- a/build.sbt +++ b/build.sbt @@ -254,6 +254,7 @@ lazy val httpMarshallersJava = project("http-marshallers-java") .enablePlugins(NoPublish /*, AggregatePRValidation*/ ) .disablePlugins(MimaPlugin) .aggregate(httpJackson) + .aggregate(httpJackson3) lazy val httpJackson = httpMarshallersJavaSubproject("jackson") @@ -264,6 +265,16 @@ lazy val httpJackson = .settings(Dependencies.httpJackson) .enablePlugins(ScaladocNoVerificationOfDiagrams) +lazy val httpJackson3 = + httpMarshallersJavaSubproject("jackson3") + .settings(AutomaticModuleName.settings("pekko.http.marshallers.jackson3")) + .addPekkoModuleDependency("pekko-stream", "provided", PekkoCoreDependency.default) + .addPekkoModuleDependency("pekko-stream-testkit", "test", PekkoCoreDependency.default) + .dependsOn(httpTestkit % "test") + .settings(Dependencies.httpJackson3) + .enablePlugins(ScaladocNoVerificationOfDiagrams) + .disablePlugins(MimaPlugin) + lazy val httpCaching = project("http-caching") .settings( name := "pekko-http-caching") diff --git a/docs/src/main/paradox/common/json-support.md b/docs/src/main/paradox/common/json-support.md index f3ab1bf839..94c95b5145 100644 --- a/docs/src/main/paradox/common/json-support.md +++ b/docs/src/main/paradox/common/json-support.md @@ -20,6 +20,8 @@ To make use of the support module for (un)marshalling from and to JSON with [Jac version="PekkoHttpVersion" } +`pekko-http-jackson` supports Jackson v2 while `pekko-http-jackson3` supports Jackson v3 (see below for details). + Use `org.apache.pekko.http.javadsl.marshallers.jackson.Jackson.unmarshaller(T.class)` to create an @apidoc[Unmarshaller[HttpEntity,T]] which expects the request body (HttpEntity) to be of type `application/json` and converts it to `T` using Jackson. @@ -30,8 +32,27 @@ Use `org.apache.pekko.http.javadsl.marshallers.jackson.Jackson.marshaller(T.clas @@snip [PetStoreExample.java](/http-tests/src/main/java/org/apache/pekko/http/javadsl/server/examples/petstore/PetStoreExample.java) { #imports #marshall } +`pekko-http-jackson` uses a Jackson `ObjectMapper` config that closely matches the Jackson default config. +If you want to control the setup of the `ObjectMapper` yourself, you can build your own +using the Jackson APIs. There are methods in `org.apache.pekko.http.javadsl.marshallers.jackson.Jackson` +that can take an `ObjectMapper` instance as input instead of having `pekko-http-jackson` build one for you. + Refer to @github[this file](/http-tests/src/main/java/org/apache/pekko/http/javadsl/server/examples/petstore/PetStoreExample.java) in the sources for the complete example. +We also now support Jackson v3. + +@@dependency [sbt,Gradle,Maven] { + bomGroup2="org.apache.pekko" bomArtifact2="pekko-http-bom_$scala.binary.version$" bomVersionSymbols2="PekkoHttpVersion" + symbol="PekkoHttpVersion" + value="$project.version$" + group="org.apache.pekko" + artifact="pekko-http-jackson3_$scala.binary.version$" + version="PekkoHttpVersion" +} + +`pekko-http-jackson3` works in much the same way as `pekko-http-jackson` but uses the newer +version of Jackson libs (`tool.jackson` jars). This lib has a class called `org.apache.pekko.http.javadsl.marshallers.jackson3.Jackson`. + @@@ diff --git a/docs/src/main/paradox/introduction.md b/docs/src/main/paradox/introduction.md index 69becb6651..a00b3df473 100644 --- a/docs/src/main/paradox/introduction.md +++ b/docs/src/main/paradox/introduction.md @@ -239,5 +239,9 @@ Details can be found here: @ref[XML Support](common/xml-support.md) @@@ @@@ div { .group-java } pekko-http-jackson -: Predefined glue-code for (de)serializing custom types from/to JSON with [jackson](https://github.com/FasterXML/jackson) +: Predefined glue-code for (de)serializing custom types from/to JSON with [jackson v2.x](https://github.com/FasterXML/jackson) +@@@ +@@@ div { .group-java } +pekko-http-jackson3 +: Predefined glue-code for (de)serializing custom types from/to JSON with [jackson v3.x](https://github.com/FasterXML/jackson) @@@ diff --git a/docs/src/main/paradox/routing-dsl/source-streaming-support.md b/docs/src/main/paradox/routing-dsl/source-streaming-support.md index 9ff29f4ef8..d311e3eb6c 100644 --- a/docs/src/main/paradox/routing-dsl/source-streaming-support.md +++ b/docs/src/main/paradox/routing-dsl/source-streaming-support.md @@ -57,7 +57,8 @@ like to stream a different content type (for example plists or protobuf). Firstly, we'll need to get some additional marshalling infrastructure set up, that is able to marshal to and from an Apache Pekko Streams @apidoc[Source[T, ?]]. Here we'll use the `Jackson` helper class from `pekko-http-jackson` (a separate library -that you should add as a dependency if you want to use Jackson with Apache Pekko HTTP). +that you should add as a dependency if you want to use Jackson v2.x with Apache Pekko HTTP). +There is also `pekko-http-jackson3` if you prefer Jackson v3.x. First we enable JSON Streaming by making an implicit @apidoc[EntityStreamingSupport] instance available (Step 1). diff --git a/http-marshallers-java/http-jackson3/src/main/java/org/apache/pekko/http/javadsl/marshallers/jackson3/Jackson.java b/http-marshallers-java/http-jackson3/src/main/java/org/apache/pekko/http/javadsl/marshallers/jackson3/Jackson.java new file mode 100644 index 0000000000..92be5a75b0 --- /dev/null +++ b/http-marshallers-java/http-jackson3/src/main/java/org/apache/pekko/http/javadsl/marshallers/jackson3/Jackson.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * license agreements; and to You under the Apache License, version 2.0: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * This file is part of the Apache Pekko project, which was derived from Akka. + */ + +/* + * Copyright (C) 2009-2022 Lightbend Inc. + */ + +package org.apache.pekko.http.javadsl.marshallers.jackson3; + +import java.io.IOException; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.apache.pekko.http.javadsl.model.HttpEntity; +import org.apache.pekko.http.javadsl.model.MediaTypes; +import org.apache.pekko.http.javadsl.model.RequestEntity; +import org.apache.pekko.http.javadsl.marshalling.Marshaller; +import org.apache.pekko.http.javadsl.unmarshalling.Unmarshaller; +import org.apache.pekko.http.scaladsl.model.ExceptionWithErrorInfo; +import org.apache.pekko.http.scaladsl.model.ErrorInfo; +import org.apache.pekko.util.ByteString; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.util.BufferRecycler; +import tools.jackson.core.util.JsonRecyclerPools; +import tools.jackson.core.util.RecyclerPool; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +/** A JSON marshaller/unmarshaller using the Jackson library. */ +public class Jackson { + private static final ObjectMapper defaultObjectMapper = createMapper(); + + /** INTERNAL API */ + public static class JacksonUnmarshallingException extends ExceptionWithErrorInfo { + public JacksonUnmarshallingException(Class expectedType, Exception cause) { + super( + new ErrorInfo( + "Cannot unmarshal JSON as " + expectedType.getSimpleName(), cause.getMessage()), + cause); + } + } + + public static Marshaller marshaller() { + return marshaller(defaultObjectMapper); + } + + public static Marshaller marshaller(ObjectMapper mapper) { + return Marshaller.wrapEntity( + u -> toJSON(mapper, u), Marshaller.stringToEntity(), MediaTypes.APPLICATION_JSON); + } + + public static Unmarshaller byteStringUnmarshaller(Class expectedType) { + return byteStringUnmarshaller(defaultObjectMapper, expectedType); + } + + public static Unmarshaller unmarshaller(Class expectedType) { + return unmarshaller(defaultObjectMapper, expectedType); + } + + public static Unmarshaller unmarshaller( + ObjectMapper mapper, Class expectedType) { + return Unmarshaller.forMediaType(MediaTypes.APPLICATION_JSON, Unmarshaller.entityToString()) + .thenApply(s -> fromJSON(mapper, s, expectedType)); + } + + public static Unmarshaller byteStringUnmarshaller( + ObjectMapper mapper, Class expectedType) { + return Unmarshaller.sync(s -> fromJSON(mapper, s.utf8String(), expectedType)); + } + + private static String toJSON(ObjectMapper mapper, Object object) { + try { + return mapper.writeValueAsString(object); + } catch (JacksonException e) { + throw new IllegalArgumentException("Cannot marshal to JSON: " + object, e); + } + } + + private static T fromJSON(ObjectMapper mapper, String json, Class expectedType) { + try { + return mapper.readerFor(expectedType).readValue(json); + } catch (JacksonException e) { + throw new JacksonUnmarshallingException(expectedType, e); + } + } + + private static ObjectMapper createMapper() { + return createMapper(ConfigFactory.load().getConfig("pekko.http.marshallers.jackson3")); + } + + static JsonFactory createJsonFactory(final Config config) { + StreamReadConstraints streamReadConstraints = + StreamReadConstraints.builder() + .maxNestingDepth(config.getInt("read.max-nesting-depth")) + .maxNumberLength(config.getInt("read.max-number-length")) + .maxStringLength(config.getInt("read.max-string-length")) + .maxNameLength(config.getInt("read.max-name-length")) + .maxDocumentLength(config.getLong("read.max-document-length")) + .maxTokenCount(config.getLong("read.max-token-count")) + .build(); + StreamWriteConstraints streamWriteConstraints = + StreamWriteConstraints.builder() + .maxNestingDepth(config.getInt("write.max-nesting-depth")) + .build(); + return JsonFactory.builder() + .streamReadConstraints(streamReadConstraints) + .streamWriteConstraints(streamWriteConstraints) + .recyclerPool(getBufferRecyclerPool(config)) + .build(); + } + + static ObjectMapper createMapper(final Config config) { + return JsonMapper.builder(createJsonFactory(config)) + // Jackson 3 changed FAIL_ON_UNKNOWN_PROPERTIES default to false + // we keep it false because it is more secure to fail on unknown properties + // and it is consistent with previous pekko-http-jackson behavior + // and there are tests depending on this behavior + .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + } + + private static RecyclerPool getBufferRecyclerPool(final Config cfg) { + final String poolType = cfg.getString("buffer-recycler.pool-instance"); + switch (poolType) { + case "thread-local": + return JsonRecyclerPools.threadLocalPool(); + case "concurrent-deque": + return JsonRecyclerPools.newConcurrentDequePool(); + case "shared-concurrent-deque": + return JsonRecyclerPools.sharedConcurrentDequePool(); + case "bounded": + return JsonRecyclerPools.newBoundedPool(cfg.getInt("buffer-recycler.bounded-pool-size")); + case "non-recycling": + return JsonRecyclerPools.nonRecyclingPool(); + default: + throw new IllegalArgumentException("Unknown recycler-pool: " + poolType); + } + } +} diff --git a/http-marshallers-java/http-jackson3/src/main/resources/reference.conf b/http-marshallers-java/http-jackson3/src/main/resources/reference.conf new file mode 100644 index 0000000000..d6368316fe --- /dev/null +++ b/http-marshallers-java/http-jackson3/src/main/resources/reference.conf @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: Apache-2.0 + +################################### +# Pekko HTTP Jackson3 Config File # +################################### + +# This is the reference config file that contains all the default settings. +# Make your edits/overrides in your application.conf. + +pekko.http.marshallers.jackson3 { + read { + # see https://www.javadoc.io/doc/tools.jackson.core/jackson-core/3.0.0/tools.jackson.core/tools/jackson/core/StreamReadConstraints.html + # these defaults are the same as the defaults in `StreamReadConstraints` + max-nesting-depth = 1000 + max-number-length = 1000 + max-string-length = 20000000 + max-name-length = 50000 + # max-document-length of -1 means unlimited + max-document-length = -1 + # max-token-count of -1 means unlimited + max-token-count = -1 + } + + write { + # see https://www.javadoc.io/doc/tools.jackson.core/jackson-core/3.0.0/tools.jackson.core/tools/jackson/core/StreamWriteConstraints.html + # these defaults are the same as the defaults in `StreamWriteConstraints` + max-nesting-depth = 1000 + } + + # Controls the Buffer Recycler Pool implementation used by Jackson. + # https://www.javadoc.io/doc/tools.jackson.core/jackson-core/3.0.0/tools.jackson.core/tools/jackson/core/util/JsonRecyclerPools.html + # The default is "thread-local" which is the same as the default in Jackson 2.18. + buffer-recycler { + # the supported values are "thread-local", "concurrent-deque", "shared-concurrent-deque", "bounded", "non-recycling" + pool-instance = "thread-local" + # the maximum size of bounded recycler pools - must be >=1 or an IllegalArgumentException will occur + # only applies to pool-instance type "bounded" + bounded-pool-size = 100 + } +} diff --git a/http-marshallers-java/http-jackson3/src/test/java/org/apache/pekko/http/javadsl/marshallers/jackson3/JacksonTest.java b/http-marshallers-java/http-jackson3/src/test/java/org/apache/pekko/http/javadsl/marshallers/jackson3/JacksonTest.java new file mode 100644 index 0000000000..ae185a87e0 --- /dev/null +++ b/http-marshallers-java/http-jackson3/src/test/java/org/apache/pekko/http/javadsl/marshallers/jackson3/JacksonTest.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * license agreements; and to You under the Apache License, version 2.0: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * This file is part of the Apache Pekko project, which was derived from Akka. + */ + +/* + * Copyright (C) 2009-2022 Lightbend Inc. + */ + +package org.apache.pekko.http.javadsl.marshallers.jackson3; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.util.BufferRecycler; +import tools.jackson.core.util.JsonRecyclerPools.BoundedPool; +import tools.jackson.core.util.RecyclerPool; +import tools.jackson.databind.ObjectMapper; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.http.javadsl.marshallers.jackson3.Jackson; +import org.apache.pekko.http.javadsl.model.ContentTypes; +import org.apache.pekko.http.javadsl.model.HttpEntities; +import org.apache.pekko.http.javadsl.model.HttpRequest; +import org.apache.pekko.http.javadsl.model.RequestEntity; +import org.apache.pekko.http.javadsl.server.Route; +import org.apache.pekko.http.javadsl.testkit.JUnitRouteTest; + +import org.junit.Test; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class JacksonTest extends JUnitRouteTest { + + public static class SomeData { + public final String field; + + @JsonCreator + public SomeData(@JsonProperty("field") String field) { + this.field = field; + } + } + + RequestEntity invalidEntity = + HttpEntities.create( + ContentTypes.APPLICATION_JSON, "{\"droids\":\"not the ones you are looking for\"}"); + + @Override + public Config additionalConfig() { + return ConfigFactory.parseString(""); + } + + @Test + public void failingToUnmarshallShouldProvideFailureDetails() throws Exception { + ActorSystem sys = ActorSystem.create("test"); + try { + CompletionStage unmarshalled = + Jackson.unmarshaller(SomeData.class).unmarshal(invalidEntity, system()); + + SomeData result = unmarshalled.toCompletableFuture().get(3, TimeUnit.SECONDS); + fail("Invalid json should not parse to object"); + } catch (ExecutionException ex) { + // CompletableFuture.get wraps in one layer of ExecutionException + String message = ex.getCause().getMessage(); + assertTrue( + "Unexpected exception message: " + message, + message.startsWith( + "Cannot unmarshal JSON as SomeData: Unrecognized property \"droids\"")); + } finally { + sys.terminate(); + } + } + + @Test + public void detailsShouldBeHiddenFromResponseEntity() throws Exception { + Route route = entity(Jackson.unmarshaller(SomeData.class), theData -> complete(theData.field)); + + runRoute(route.seal(), HttpRequest.PUT("/").withEntity(invalidEntity)) + .assertEntity("The request content was malformed:\nCannot unmarshal JSON as SomeData"); + } + + @Test + public void configStreamReadsConstraints() throws Exception { + final int maxNumLen = 987; + final int maxNameLen = 54321; + final int maxStringLen = 1234567; + final long maxDocLen = 123456789L; + final long maxTokenCount = 9876543210L; + final int maxNestingDepth = 5; + String configText = + "read.max-number-length=" + + maxNumLen + + "\n" + + "read.max-name-length=" + + maxNameLen + + "\n" + + "read.max-string-length=" + + maxStringLen + + "\n" + + "read.max-document-length=" + + maxDocLen + + "\n" + + "read.max-token-count=" + + maxTokenCount + + "\n" + + "read.max-nesting-depth=" + + maxNestingDepth; + Config config = ConfigFactory.parseString(configText).withFallback(getDefaultConfig()); + JsonFactory jsonFactory = Jackson.createJsonFactory(config); + StreamReadConstraints constraints = jsonFactory.streamReadConstraints(); + assertEquals(maxNumLen, constraints.getMaxNumberLength()); + assertEquals(maxNameLen, constraints.getMaxNameLength()); + assertEquals(maxStringLen, constraints.getMaxStringLength()); + assertEquals(maxDocLen, constraints.getMaxDocumentLength()); + assertEquals(maxTokenCount, constraints.getMaxTokenCount()); + assertEquals(maxNestingDepth, constraints.getMaxNestingDepth()); + } + + @Test + public void configStreamWritesConstraints() throws Exception { + final int maxNestingDepth = 5; + String configText = "write.max-nesting-depth=" + maxNestingDepth; + Config config = ConfigFactory.parseString(configText).withFallback(getDefaultConfig()); + JsonFactory jsonFactory = Jackson.createJsonFactory(config); + StreamWriteConstraints constraints = jsonFactory.streamWriteConstraints(); + assertEquals(maxNestingDepth, constraints.getMaxNestingDepth()); + } + + @Test + public void testDefaultFactory() throws Exception { + JsonFactory jsonFactory = Jackson.createJsonFactory(getDefaultConfig()); + RecyclerPool recyclerPool = jsonFactory._getRecyclerPool(); + assertEquals("ThreadLocalPool", recyclerPool.getClass().getSimpleName()); + } + + @Test + public void testFactoryWithBufferRecyclerSetting() throws Exception { + final String poolType = "bounded"; + final int poolSize = 10; + String configText = + "buffer-recycler.pool-instance=" + + poolType + + "\nbuffer-recycler.bounded-pool-size=" + + poolSize; + Config config = ConfigFactory.parseString(configText).withFallback(getDefaultConfig()); + JsonFactory jsonFactory = Jackson.createJsonFactory(config); + RecyclerPool recyclerPool = jsonFactory._getRecyclerPool(); + assertEquals("BoundedPool", recyclerPool.getClass().getSimpleName()); + assertEquals(poolSize, ((BoundedPool) recyclerPool).capacity()); + } + + private static Config getDefaultConfig() { + return ConfigFactory.load().getConfig("pekko.http.marshallers.jackson3"); + } +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c9959d9d2f..e6aab354e3 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -19,6 +19,7 @@ object Dependencies { import DependencyHelpers._ val jacksonDatabindVersion = "2.20.0" + val jacksonDatabind3Version = "3.0.0" val jacksonXmlVersion = jacksonDatabindVersion val junitVersion = "4.13.2" val h2specVersion = "2.6.0" @@ -58,6 +59,9 @@ object Dependencies { // For pekko-http-jackson support val jacksonDatabind = "com.fasterxml.jackson.core" % "jackson-databind" % jacksonDatabindVersion + // For pekko-http-jackson3 support + val jacksonDatabind3 = "tools.jackson.core" % "jackson-databind" % jacksonDatabind3Version + // For pekko-http-testkit-java val junit = "junit" % "junit" % junitVersion @@ -139,6 +143,8 @@ object Dependencies { lazy val httpJackson = l ++= Seq(jacksonDatabind, Test.scalatestplusJUnit, Test.junit, Test.junitIntf) + lazy val httpJackson3 = l ++= Seq(jacksonDatabind3, Test.scalatestplusJUnit, Test.junit, Test.junitIntf) + lazy val docs = l ++= Seq(Docs.sprayJson, Docs.gson, Docs.jacksonXml, Docs.reflections) }