Support GSON media (#5090)

* Support GSON media

Signed-off-by: Jorge Bescos Gascon <jorge.bescos.gascon@oracle.com>
diff --git a/bom/pom.xml b/bom/pom.xml
index 23dbb16..0cd67c4 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -298,6 +298,11 @@
             </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.media</groupId>
+                <artifactId>jersey-media-json-gson</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.media</groupId>
                 <artifactId>jersey-media-json-binding</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/media/json-gson/pom.xml b/media/json-gson/pom.xml
new file mode 100644
index 0000000..d1b45ea
--- /dev/null
+++ b/media/json-gson/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.glassfish.jersey.media</groupId>
+        <artifactId>project</artifactId>
+        <version>2.37-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>jersey-media-json-gson</artifactId>
+    <packaging>jar</packaging>
+    <name>jersey-media-json-gson</name>
+
+    <description>
+        Jersey GSON support module.
+    </description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.sun.istack</groupId>
+                <artifactId>istack-commons-maven-plugin</artifactId>
+                <inherited>true</inherited>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <inherited>true</inherited>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <inherited>true</inherited>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Export-Package>org.glassfish.jersey.gson.*</Export-Package>
+                        <Import-Package>${javax.annotation.osgi.version},*</Import-Package>
+                    </instructions>
+                    <unpackBundle>true</unpackBundle>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/JsonGsonFeature.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/JsonGsonFeature.java
new file mode 100644
index 0000000..9e5cc55
--- /dev/null
+++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/JsonGsonFeature.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.gson;
+
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.Feature;
+import javax.ws.rs.core.FeatureContext;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.gson.internal.JsonGsonAutoDiscoverable;
+import org.glassfish.jersey.gson.internal.JsonGsonProvider;
+import org.glassfish.jersey.internal.InternalProperties;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+/**
+ * Feature used to register Gson providers.
+ * <p>
+ * The Feature is automatically enabled when {@link JsonGsonAutoDiscoverable} is on classpath.
+ * Default GSON configuration obtained by calling {@code GsonBuilder.create()} is used.
+ * <p>
+ * Custom configuration, if required, can be achieved by implementing custom {@link javax.ws.rs.ext.ContextResolver} and
+ * registering it as a provider into JAX-RS runtime:
+ * <pre>
+ * &#64;Provider
+ * &#64;class GsonContextResolver implements ContextResolver&lt;Gson&gt; {
+ *      &#64;Override
+ *      public Gson getContext(Class<?> type) {
+ *          GsonBuilder builder = new GsonBuilder();
+ *          // add custom configuration
+ *          return builder.create();
+ *      }
+ * }
+ * </pre>
+ *
+ */
+public class JsonGsonFeature implements Feature {
+
+    private static final String JSON_FEATURE = JsonGsonFeature.class.getSimpleName();
+
+    @Override
+    public boolean configure(final FeatureContext context) {
+        final Configuration config = context.getConfiguration();
+
+        final String jsonFeature = CommonProperties.getValue(
+                config.getProperties(),
+                config.getRuntimeType(),
+                InternalProperties.JSON_FEATURE, JSON_FEATURE, String.class);
+
+        // Other JSON providers registered.
+        if (!JSON_FEATURE.equalsIgnoreCase(jsonFeature)) {
+            return false;
+        }
+
+        // Disable other JSON providers.
+        context.property(PropertiesHelper.getPropertyNameForRuntime(
+                InternalProperties.JSON_FEATURE, config.getRuntimeType()), JSON_FEATURE);
+
+        context.register(JsonGsonProvider.class);
+
+        return true;
+    }
+}
diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonAutoDiscoverable.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonAutoDiscoverable.java
new file mode 100644
index 0000000..b9fa6e2
--- /dev/null
+++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonAutoDiscoverable.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.gson.internal;
+
+import javax.annotation.Priority;
+import javax.ws.rs.core.FeatureContext;
+
+import org.glassfish.jersey.gson.JsonGsonFeature;
+import org.glassfish.jersey.internal.spi.AutoDiscoverable;
+import org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable;
+
+/**
+ * {@link ForcedAutoDiscoverable} registering {@link JsonGsonFeature} if the feature is not already registered.
+ * <p>
+ *
+ * @see JsonGsonFeature
+ */
+@Priority(AutoDiscoverable.DEFAULT_PRIORITY)
+public class JsonGsonAutoDiscoverable implements ForcedAutoDiscoverable {
+
+    @Override
+    public void configure(FeatureContext context) {
+        if (!context.getConfiguration().isRegistered(JsonGsonFeature.class)) {
+            context.register(JsonGsonFeature.class);
+        }
+    }
+}
diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java
new file mode 100644
index 0000000..3799af1
--- /dev/null
+++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.gson.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.NoContentException;
+import javax.ws.rs.ext.ContextResolver;
+import javax.ws.rs.ext.Provider;
+import javax.ws.rs.ext.Providers;
+
+import org.glassfish.jersey.gson.LocalizationMessages;
+import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider;
+import org.glassfish.jersey.message.internal.EntityInputStream;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * Entity provider (reader and writer) for Gson.
+ *
+ */
+@Provider
+@Produces({"application/json", "text/json", "*/*"})
+@Consumes({"application/json", "text/json", "*/*"})
+public class JsonGsonProvider extends AbstractMessageReaderWriterProvider<Object> {
+
+    private static final String JSON = "json";
+    private static final String PLUS_JSON = "+json";
+
+    private Providers providers;
+
+    public JsonGsonProvider(@Context Providers providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return supportsMediaType(mediaType);
+    }
+
+    @Override
+    public Object readFrom(Class<Object> type, Type genericType,
+                           Annotation[] annotations,
+                           MediaType mediaType,
+                           MultivaluedMap<String, String> httpHeaders,
+                           InputStream entityStream) throws IOException, WebApplicationException {
+        EntityInputStream entityInputStream =  new EntityInputStream(entityStream);
+        entityStream = entityInputStream;
+        if (entityInputStream.isEmpty()) {
+            throw new NoContentException(LocalizationMessages.ERROR_GSON_EMPTYSTREAM());
+        }
+        Gson gson = getGson(type);
+        try {
+            return gson.fromJson(new InputStreamReader(entityInputStream,
+                    AbstractMessageReaderWriterProvider.getCharset(mediaType)), genericType);
+        } catch (Exception e) {
+            throw new ProcessingException(LocalizationMessages.ERROR_GSON_DESERIALIZATION(), e);
+        }
+    }
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return supportsMediaType(mediaType);
+    }
+
+    @Override
+    public void writeTo(Object o, Class<?> type, Type genericType,
+                        Annotation[] annotations,
+                        MediaType mediaType,
+                        MultivaluedMap<String, Object> httpHeaders,
+                        OutputStream entityStream) throws IOException, WebApplicationException {
+        Gson gson = getGson(type);
+        try {
+            entityStream.write(gson.toJson(o).getBytes(AbstractMessageReaderWriterProvider.getCharset(mediaType)));
+            entityStream.flush();
+        } catch (Exception e) {
+            throw new ProcessingException(LocalizationMessages.ERROR_GSON_SERIALIZATION(), e);
+        }
+    }
+
+    private Gson getGson(Class<?> type) {
+        final ContextResolver<Gson> contextResolver = providers.getContextResolver(Gson.class, MediaType.APPLICATION_JSON_TYPE);
+        if (contextResolver != null) {
+            return contextResolver.getContext(type);
+        } else {
+            return GsonSingleton.INSTANCE.getInstance();
+        }
+    }
+
+    /**
+     * @return true for all media types of the pattern *&#47;json and
+     * *&#47;*+json.
+     */
+    private static boolean supportsMediaType(final MediaType mediaType) {
+        return mediaType.getSubtype().equals(JSON) || mediaType.getSubtype().endsWith(PLUS_JSON);
+    }
+
+    private enum GsonSingleton {
+        INSTANCE;
+
+        // Thread-safe
+        private Gson gsonInstance;
+
+        Gson getInstance() {
+            return gsonInstance;
+        }
+
+        GsonSingleton() {
+            this.gsonInstance = new GsonBuilder().create();
+        }
+    }
+}
diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/package-info.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/package-info.java
new file mode 100644
index 0000000..12e6b90
--- /dev/null
+++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey classes supporting JSON marshalling and unmarshalling using GSON.
+ */
+package org.glassfish.jersey.gson;
diff --git a/media/json-gson/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable b/media/json-gson/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable
new file mode 100644
index 0000000..7f4e0ee
--- /dev/null
+++ b/media/json-gson/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.ForcedAutoDiscoverable
@@ -0,0 +1 @@
+org.glassfish.jersey.gson.internal.JsonGsonAutoDiscoverable
diff --git a/media/json-gson/src/main/resources/org/glassfish/jersey/gson/localization.properties b/media/json-gson/src/main/resources/org/glassfish/jersey/gson/localization.properties
new file mode 100644
index 0000000..b2ea26a
--- /dev/null
+++ b/media/json-gson/src/main/resources/org/glassfish/jersey/gson/localization.properties
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Eclipse Public License v. 2.0, which is available at
+# http://www.eclipse.org/legal/epl-2.0.
+#
+# This Source Code may also be made available under the following Secondary
+# Licenses when the conditions for such availability set forth in the
+# Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+# version 2 with the GNU Classpath Exception, which is available at
+# https://www.gnu.org/software/classpath/license.html.
+#
+# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+#
+
+error.gson.serialization=Error writing GSON serialized object.
+error.gson.deserialization=Error deserializing object from entity stream.
+error.gson.emptystream=GSON cannot parse empty input stream.
diff --git a/media/json-gson/src/test/java/org/glassfish/jersey/gson/internal/JsonGsonProviderTest.java b/media/json-gson/src/test/java/org/glassfish/jersey/gson/internal/JsonGsonProviderTest.java
new file mode 100644
index 0000000..c7884d6
--- /dev/null
+++ b/media/json-gson/src/test/java/org/glassfish/jersey/gson/internal/JsonGsonProviderTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.gson.internal;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.NoContentException;
+import javax.ws.rs.ext.ContextResolver;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Providers;
+
+import org.junit.Test;
+
+public class JsonGsonProviderTest {
+
+    @Test(expected = NoContentException.class)
+    public void shouldThrowNoContentException() throws IOException {
+        Providers providers = new EmptyProviders();
+        MessageBodyReader<Foo> mbr = (MessageBodyReader) new JsonGsonProvider(providers);
+        mbr.readFrom(Foo.class, Foo.class, new Annotation[0], APPLICATION_JSON_TYPE,
+                new MultivaluedHashMap<>(), new ByteArrayInputStream(new byte[0]));
+    }
+
+    private static final class Foo {
+        // no members
+    }
+
+    private static final class EmptyProviders implements Providers {
+
+        @Override
+        public final <T> MessageBodyReader<T> getMessageBodyReader(final Class<T> type, final Type genericType,
+                final Annotation[] annotations, final MediaType mediaType) {
+            return null;
+        }
+
+        @Override
+        public final <T> MessageBodyWriter<T> getMessageBodyWriter(final Class<T> type, final Type genericType,
+                final Annotation[] annotations, final MediaType mediaType) {
+            return null;
+        }
+
+        @Override
+        public final <T extends Throwable> ExceptionMapper<T> getExceptionMapper(final Class<T> type) {
+            return null;
+        }
+
+        @Override
+        public final <T> ContextResolver<T> getContextResolver(final Class<T> contextType, final MediaType mediaType) {
+            return null;
+        }
+
+    }
+
+}
diff --git a/media/pom.xml b/media/pom.xml
index 20fc3b4..4f0d77f 100644
--- a/media/pom.xml
+++ b/media/pom.xml
@@ -38,6 +38,7 @@
     <modules>
         <module>jaxb</module>
         <module>json-binding</module>
+        <module>json-gson</module>
         <module>json-jackson</module>
         <module>json-jackson1</module>
         <module>json-jettison</module>
diff --git a/pom.xml b/pom.xml
index 2466f40..4047aa1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2061,6 +2061,12 @@
                 <artifactId>yasson</artifactId>
                 <version>${yasson.version}</version>
             </dependency>
+            
+            <dependency>
+               <groupId>com.google.code.gson</groupId>
+               <artifactId>gson</artifactId>
+               <version>${gson.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>io.opentracing</groupId>
@@ -2205,5 +2211,6 @@
         <servlet3.version>3.0.1</servlet3.version>
         <servlet4.version>4.0.4</servlet4.version>
         <yasson.version>1.0.11</yasson.version>
+        <gson.version>2.9.0</gson.version>
     </properties>
 </project>
diff --git a/tests/e2e-server/pom.xml b/tests/e2e-server/pom.xml
index 6b8f3aa..2650a3d 100644
--- a/tests/e2e-server/pom.xml
+++ b/tests/e2e-server/pom.xml
@@ -101,6 +101,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-gson</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.glassfish.jersey.ext</groupId>
             <artifactId>jersey-bean-validation</artifactId>
             <scope>test</scope>
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonCustomTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonCustomTest.java
new file mode 100644
index 0000000..3d9f085
--- /dev/null
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonCustomTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.server;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Date;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ContextResolver;
+import javax.ws.rs.ext.Provider;
+
+import org.glassfish.jersey.gson.JsonGsonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class GsonCustomTest extends JerseyTest {
+
+    private static final Date date = new Date(0);
+
+    @Path("/test")
+    public static class Resource {
+
+        @GET
+        @Consumes("application/json")
+        public Date get() {
+            return date;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Resource.class).register(JsonGsonFeature.class).register(GsonContextResolver.class);
+    }
+
+    @Test
+    public void get() {
+        Response response = target("/test").request().get();
+        assertEquals(200, response.getStatus());
+        String obj = response.readEntity(String.class);
+        assertEquals("\"1970\"", obj);
+    }
+
+    @Provider
+    public static class GsonContextResolver implements ContextResolver<Gson> {
+        @Override
+        public Gson getContext(Class<?> type) {
+            GsonBuilder builder = new GsonBuilder();
+            builder.setDateFormat("yyyy");
+            return builder.create();
+        }
+    }
+}
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonDefaultTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonDefaultTest.java
new file mode 100644
index 0000000..724a328
--- /dev/null
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/GsonDefaultTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.server;
+
+import static org.junit.Assert.assertEquals;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.gson.JsonGsonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Test;
+
+public class GsonDefaultTest extends JerseyTest {
+
+    @Path("/test")
+    public static class Resource {
+
+        @POST
+        @Consumes("application/json")
+        @Produces("application/json")
+        public Obj post(Obj entity) {
+            entity.setValue("bar");
+            return entity;
+        }
+
+        @GET
+        @Consumes("application/json")
+        public Obj get() {
+            Obj entity = new Obj();
+            entity.setValue("get");
+            return entity;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Resource.class)
+                .register(JsonGsonFeature.class);
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.register(JsonGsonFeature.class);
+    }
+
+    @Test
+    public void get() {
+        Response response = target("/test").request().get();
+        assertEquals(200, response.getStatus());
+        Obj obj = response.readEntity(Obj.class);
+        assertEquals("get", obj.getValue());
+    }
+
+    @Test
+    public void post() {
+        Obj obj = new Obj();
+        obj.setValue("foo");
+        Response response = target("/test").request().post(Entity.json(obj));
+        assertEquals(200, response.getStatus());
+        obj = response.readEntity(Obj.class);
+        assertEquals("bar", obj.getValue());
+    }
+
+    public static class Obj {
+        private String value;
+
+        public String getValue() {
+            return value;
+        }
+        public void setValue(String value) {
+            this.value = value;
+        }
+    }
+}