diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml
new file mode 100644
index 00000000..2e0e09b3
--- /dev/null
+++ b/extensions/gson/pom.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ 4.0.0
+
+
+ io.jsonwebtoken
+ jjwt-root
+ 0.10.8-SNAPSHOT
+ ../../pom.xml
+
+
+ jjwt-gson
+ JJWT :: Extensions :: Gson
+ jar
+
+
+ ${basedir}/../..
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+
+
+ com.google.code.gson
+ gson
+
+
+
+
\ No newline at end of file
diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java
new file mode 100644
index 00000000..b3dfc843
--- /dev/null
+++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java
@@ -0,0 +1,44 @@
+package io.jsonwebtoken.gson.io;
+
+import com.google.gson.Gson;
+import io.jsonwebtoken.io.DeserializationException;
+import io.jsonwebtoken.io.Deserializer;
+import io.jsonwebtoken.lang.Assert;
+import java.io.IOException;
+
+public class GsonDeserializer implements Deserializer {
+
+ private final Class returnType;
+ private final Gson gson;
+
+ @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
+ public GsonDeserializer() {
+ this(GsonSerializer.DEFAULT_GSON);
+ }
+
+ @SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom gson
+ public GsonDeserializer(Gson gson) {
+ this(gson, (Class) Object.class);
+ }
+
+ private GsonDeserializer(Gson gson, Class returnType) {
+ Assert.notNull(gson, "gson cannot be null.");
+ Assert.notNull(returnType, "Return type cannot be null.");
+ this.gson = gson;
+ this.returnType = returnType;
+ }
+
+ @Override
+ public T deserialize(byte[] bytes) throws DeserializationException {
+ try {
+ return readValue(bytes);
+ } catch (IOException e) {
+ String msg = "Unable to deserialize bytes into a " + returnType.getName() + " instance: " + e.getMessage();
+ throw new DeserializationException(msg, e);
+ }
+ }
+
+ protected T readValue(byte[] bytes) throws IOException {
+ return gson.fromJson(new String(bytes), returnType);
+ }
+}
diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java
new file mode 100644
index 00000000..f3050690
--- /dev/null
+++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java
@@ -0,0 +1,50 @@
+package io.jsonwebtoken.gson.io;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import io.jsonwebtoken.io.Encoders;
+import io.jsonwebtoken.io.SerializationException;
+import io.jsonwebtoken.io.Serializer;
+import io.jsonwebtoken.lang.Assert;
+import io.jsonwebtoken.lang.Strings;
+
+public class GsonSerializer implements Serializer {
+
+ static final Gson DEFAULT_GSON = new GsonBuilder().disableHtmlEscaping().create();
+ private Gson gson;
+
+ @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
+ public GsonSerializer() {
+ this(DEFAULT_GSON);
+ }
+
+ @SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom gson
+ public GsonSerializer(Gson gson) {
+ Assert.notNull(gson, "gson cannot be null.");
+ this.gson = gson;
+ }
+
+ @Override
+ public byte[] serialize(T t) throws SerializationException {
+ Assert.notNull(t, "Object to serialize cannot be null.");
+ try {
+ return writeValueAsBytes(t);
+ } catch (Exception e) {
+ String msg = "Unable to serialize object: " + e.getMessage();
+ throw new SerializationException(msg, e);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") //for testing
+ protected byte[] writeValueAsBytes(T t) {
+ Object o;
+ if (t instanceof byte[]) {
+ o = Encoders.BASE64.encode((byte[]) t);
+ } else if (t instanceof char[]) {
+ o = new String((char[]) t);
+ } else {
+ o = t;
+ }
+ return this.gson.toJson(o).getBytes(Strings.UTF_8);
+ }
+}
diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy
new file mode 100644
index 00000000..40c5caf8
--- /dev/null
+++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy
@@ -0,0 +1,65 @@
+package io.jsonwebtoken.gson.io
+
+import com.google.gson.Gson
+import io.jsonwebtoken.io.DeserializationException
+import io.jsonwebtoken.lang.Strings
+import org.junit.Test
+
+import static org.easymock.EasyMock.*
+import static org.junit.Assert.*
+
+class GsonDeserializerTest {
+
+ @Test
+ void testDefaultConstructor() {
+ def deserializer = new GsonDeserializer()
+ assertNotNull deserializer.gson
+ }
+
+ @Test
+ void testObjectMapperConstructor() {
+ def customGSON = new Gson()
+ def deserializer = new GsonDeserializer(customGSON)
+ assertSame customGSON, deserializer.gson
+ }
+
+ @Test(expected = IllegalArgumentException)
+ void testObjectMapperConstructorWithNullArgument() {
+ new GsonDeserializer<>(null)
+ }
+
+ @Test
+ void testDeserialize() {
+ byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
+ def expected = [hello: '世界']
+ def result = new GsonDeserializer().deserialize(serialized)
+ assertEquals expected, result
+ }
+
+ @Test
+ void testDeserializeFailsWithJsonProcessingException() {
+
+ def ex = createMock(java.io.IOException)
+
+ expect(ex.getMessage()).andReturn('foo')
+
+ def deserializer = new GsonDeserializer() {
+ @Override
+ protected Object readValue(byte[] bytes) throws java.io.IOException {
+ throw ex
+ }
+ }
+
+ replay ex
+
+ try {
+ deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8))
+ fail()
+ } catch (DeserializationException se) {
+ assertEquals 'Unable to deserialize bytes into a java.lang.Object instance: foo', se.getMessage()
+ assertSame ex, se.getCause()
+ }
+
+ verify ex
+ }
+}
diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy
new file mode 100644
index 00000000..b65d68a9
--- /dev/null
+++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy
@@ -0,0 +1,100 @@
+package io.jsonwebtoken.gson.io
+
+import io.jsonwebtoken.lang.Strings
+import org.junit.Test
+
+import static org.easymock.EasyMock.*
+import static org.junit.Assert.*
+import com.google.gson.Gson
+import io.jsonwebtoken.io.SerializationException
+
+class GsonSerializerTest {
+
+ @Test
+ void testDefaultConstructor() {
+ def serializer = new GsonSerializer()
+ assertNotNull serializer.gson
+ }
+
+ @Test
+ void testObjectMapperConstructor() {
+ def customGSON = new Gson()
+ def serializer = new GsonSerializer<>(customGSON)
+ assertSame customGSON, serializer.gson
+ }
+
+ @Test(expected = IllegalArgumentException)
+ void testObjectMapperConstructorWithNullArgument() {
+ new GsonSerializer<>(null)
+ }
+
+ @Test
+ void testByte() {
+ byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120
+ byte[] bytes = "x".getBytes(Strings.UTF_8)
+ byte[] result = new GsonSerializer().serialize(bytes[0]) //single byte
+ assertTrue Arrays.equals(expected, result)
+ }
+
+ @Test
+ void testByteArray() { //expect Base64 string by default:
+ byte[] bytes = "hi".getBytes(Strings.UTF_8)
+ String expected = '"aGk="' as String //base64(hi) --> aGk=
+ byte[] result = new GsonSerializer().serialize(bytes)
+ assertEquals expected, new String(result, Strings.UTF_8)
+ }
+
+ @Test
+ void testEmptyByteArray() { //expect Base64 string by default:
+ byte[] bytes = new byte[0]
+ byte[] result = new GsonSerializer().serialize(bytes)
+ assertEquals '""', new String(result, Strings.UTF_8)
+ }
+
+ @Test
+ void testChar() { //expect Base64 string by default:
+ byte[] result = new GsonSerializer().serialize('h' as char)
+ assertEquals "\"h\"", new String(result, Strings.UTF_8)
+ }
+
+ @Test
+ void testCharArray() { //expect Base64 string by default:
+ byte[] result = new GsonSerializer().serialize("hi".toCharArray())
+ assertEquals "\"hi\"", new String(result, Strings.UTF_8)
+ }
+
+ @Test
+ void testSerialize() {
+ byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
+ byte[] result = new GsonSerializer().serialize([hello: '世界'])
+ assertTrue Arrays.equals(expected, result)
+ }
+
+
+ @Test
+ void testSerializeFailsWithJsonProcessingException() {
+
+ def ex = createMock(SerializationException)
+
+ expect(ex.getMessage()).andReturn('foo')
+
+ def serializer = new GsonSerializer() {
+ @Override
+ protected byte[] writeValueAsBytes(Object o) throws SerializationException {
+ throw ex
+ }
+ }
+
+ replay ex
+
+ try {
+ serializer.serialize([hello: 'world'])
+ fail()
+ } catch (SerializationException se) {
+ assertEquals 'Unable to serialize object: foo', se.getMessage()
+ assertSame ex, se.getCause()
+ }
+
+ verify ex
+ }
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 90888ae6..0cf57558 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -36,6 +36,7 @@
jackson
orgjson
+ gson
\ No newline at end of file
diff --git a/impl/pom.xml b/impl/pom.xml
index 951b1f07..2fb4bbe4 100644
--- a/impl/pom.xml
+++ b/impl/pom.xml
@@ -54,6 +54,11 @@
jjwt-orgjson
test
+
+ io.jsonwebtoken
+ jjwt-gson
+ test
+
\ No newline at end of file
diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java
index 034fe34e..022bb289 100644
--- a/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java
+++ b/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java
@@ -34,6 +34,8 @@ public class RuntimeClasspathDeserializerLocator implements InstanceLocator2.9.9.1
20180130
+ 2.8.5
1.60
@@ -131,6 +132,11 @@
jjwt-orgjson
${project.version}
+
+ io.jsonwebtoken
+ jjwt-gson
+ ${project.version}
+
com.fasterxml.jackson.core
jackson-databind
@@ -141,6 +147,11 @@
json
${orgjson.version}
+
+ com.google.code.gson
+ gson
+ ${gson.version}
+