Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Version 5.4.0
* Add `BasicAuthRequestInterceptor`
* Add Jackson integration

### Version 5.3.0
* Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ GitHub github = Feign.builder()
.target(GitHub.class, "https://api.github.com");
```

### Jackson
[JacksonModule](https://github.com/Netflix/feign/tree/master/jackson) adds an encoder and decoder you can use with a JSON API.

Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:

```java
GitHub github = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
```

### Sax
[SaxDecoder](https://github.com/Netflix/feign/tree/master/sax) allows you to decode XML in a way that is compatible with normal JVM and also Android environments.

Expand Down
15 changes: 15 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ project(':feign-gson') {
}
}

project(':feign-jackson') {
apply plugin: 'java'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adriancole
Not really a criticism of the pull request (since it appropriately follows the pattern used in the rest of the file), but is there a reason we apply the java plugin in each separate project block rather than in an allprojects or subprojects block?

http://www.gradle.org/docs/current/userguide/multi_project_builds.html

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android isnt compatible with the java plugin. That said, we don't have an
android module :)


test {
useTestNG()
}

dependencies {
compile project(':feign-core')
compile 'com.fasterxml.jackson.core:jackson-databind:2.2.2'
testCompile 'org.testng:testng:6.8.5'
testCompile 'com.google.guava:guava:14.0.1'
}
}

project(':feign-jaxrs') {
apply plugin: 'java'

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/feign/codec/DecodeException.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
/**
* Similar to {@code javax.websocket.DecodeException}, raised when a problem
* occurs decoding a message. Note that {@code DecodeException} is not an
* {@code IOException}, nor have one set as its cause.
* {@code IOException}, nor does it have one set as its cause.
*/
public class DecodeException extends FeignException {

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/feign/codec/EncodeException.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

/**
* Similar to {@code javax.websocket.EncodeException}, raised when a problem
* occurs decoding a message. Note that {@code DecodeException} is not an
* {@code IOException}, nor have one set as its cause.
* occurs encoding a message. Note that {@code EncodeException} is not an
* {@code IOException}, nor does it have one set as its cause.
*/
public class EncodeException extends FeignException {

Expand Down
33 changes: 33 additions & 0 deletions jackson/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Jackson Codec
===================

This module adds support for encoding and decoding JSON via Jackson.

Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:

```java
GitHub github = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
```

If you want to customize the `ObjectMapper` that is used, provide it to the `JacksonEncoder` and `JacksonDecoder`:

```java
ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

GitHub github = Feign.builder()
.encoder(new JacksonEncoder(mapper))
.decoder(new JacksonDecoder(mapper))
.target(GitHub.class, "https://api.github.com");
```

Alternatively, you can add the encoder and decoder to your Dagger object graph using the provided `JacksonModule` like so:

```java
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new JacksonModule());
```
53 changes: 53 additions & 0 deletions jackson/src/main/java/feign/jackson/JacksonDecoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.jackson;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import feign.Response;
import feign.codec.Decoder;

import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;

public class JacksonDecoder implements Decoder {
private final ObjectMapper mapper;

public JacksonDecoder() {
this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false));
}

public JacksonDecoder(ObjectMapper mapper) {
this.mapper = mapper;
}

@Override public Object decode(Response response, Type type) throws IOException {
if (response.body() == null) {
return null;
}
Reader reader = response.body().asReader();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question: is there a way to get InputStream? That would be more efficient input source since Jackson combines UTF-8 decoding and parsing into a single step with raw byte-based input. It will also auto-detect encoding, as per JSON specification recommentation (... if anyone, ever, didn't use UTF-8 anyway :) ).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, currently there is no way to get an InputStream.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#83 is for binary bodies, but not until 6.0. Probably best to leave this as reader and then add another PR to revise after issue #83

try {
return mapper.readValue(reader, mapper.constructType(type));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there are no jackson runtime exceptions thrown here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a com.fasterxml.jackson.databind.RuntimeJsonMappingException is a possibility. Shall I catch that and inspect whether the cause is an IOException in the same fashion as the GsonDecoder?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If jackson wraps ioexception by design, then unwrap. Otherwise just coerce
to DecodeException.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done; it does wrap IOException by design, so I've added code to unwrap it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jackson's own exceptions all extend IOException, FWIW. Raw IOExceptions are actually not wrapped by default; not sure if that matters greatly. It is mostly for convenience of catching failures from different levels: JsonParsingException vs JsonMappingException vs "raw" IOException.
I guess I mention this just for sake of completeness here. :-)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx

} catch (RuntimeJsonMappingException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably clarify decoder javadoc that you dont need defensive finally close blocks.

}
}
46 changes: 46 additions & 0 deletions jackson/src/main/java/feign/jackson/JacksonEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.jackson;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;

public class JacksonEncoder implements Encoder {
private final ObjectMapper mapper;

public JacksonEncoder() {
this(new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true));
}

public JacksonEncoder(ObjectMapper mapper) {
this.mapper = mapper;
}

@Override public void encode(Object object, RequestTemplate template) throws EncodeException {
try {
template.body(mapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
throw new EncodeException(e.getMessage(), e);
}
}
}
103 changes: 103 additions & 0 deletions jackson/src/main/java/feign/jackson/JacksonModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.jackson;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import dagger.Provides;
import feign.Feign;
import feign.codec.Decoder;
import feign.codec.Encoder;

import javax.inject.Singleton;
import java.util.Collections;
import java.util.Set;

/**
* <h3>Custom serializers/deserializers</h3>
* <br>
* In order to specify custom json parsing, Jackson's {@code ObjectMapper} supports {@link JsonSerializer serializers}
* and {@link JsonDeserializer deserializers}, which can be bundled together in {@link Module modules}.
* <p/>
* <br>
* Here's an example of adding a custom module.
* <p/>
* <pre>
* public class ObjectIdSerializer extends StdSerializer&lt;ObjectId&gt; {
* public ObjectIdSerializer() {
* super(ObjectId.class);
* }
*
* &#064;Override
* public void serialize(ObjectId value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
* jsonGenerator.writeString(value.toString());
* }
* }
*
* public class ObjectIdDeserializer extends StdDeserializer&lt;ObjectId&gt; {
* public ObjectIdDeserializer() {
* super(ObjectId.class);
* }
*
* &#064;Override
* public ObjectId deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
* return ObjectId.massageToObjectId(jsonParser.getValueAsString());
* }
* }
*
* public class ObjectIdModule extends SimpleModule {
* public ObjectIdModule() {
* // first deserializers
* addDeserializer(ObjectId.class, new ObjectIdDeserializer());
*
* // then serializers:
* addSerializer(ObjectId.class, new ObjectIdSerializer());
* }
* }
*
* &#064;Provides(type = Provides.Type.SET)
* Module objectIdModule() {
* return new ObjectIdModule();
* }
* </pre>
*/
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
public final class JacksonModule {
@Provides Encoder encoder(ObjectMapper mapper) {
return new JacksonEncoder(mapper);
}

@Provides Decoder decoder(ObjectMapper mapper) {
return new JacksonDecoder(mapper);
}

@Provides @Singleton ObjectMapper mapper(Set<Module> modules) {
return new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModules(modules);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these default ObjectMapper tweaks also appropriate for the default ObjectMapper used by the encoder and decoder outside of Dagger?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably so; I'll modify the default constructors of the Encoder and Decoder as such.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've modified the JacksonEncoder and JacksonDecoder to apply the same default ObjectMapper tweaks that are used in the JacksonModule.

}

@Provides(type = Provides.Type.SET_VALUES) Set<Module> noDefaultModules() {
return Collections.emptySet();
}
}
Loading