Feign's codec system provides pluggable request encoding and response decoding through the Encoder and Decoder interfaces, enabling support for various data formats and custom serialization logic.
Request body encoding interface for converting objects to request bodies.
/**
* Encodes objects into HTTP request bodies
*/
public interface Encoder {
/**
* Encode object to request body
* @param object Object to encode
* @param bodyType Type of the object
* @param template Request template to modify
* @throws EncodeException if encoding fails
*/
void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
/**
* Default encoder that handles String and byte[] objects
*/
public static class Default implements Encoder {
/** Encode String and byte[] objects, throw exception for others */
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
}
}
/**
* Exception thrown when encoding fails
*/
public class EncodeException extends FeignException {
public EncodeException(String message);
public EncodeException(String message, Throwable cause);
}Response body decoding interface for converting HTTP responses to objects.
/**
* Decodes HTTP response bodies into objects
*/
public interface Decoder {
/**
* Decode response body to object
* @param response HTTP response
* @param type Target type to decode to
* @return Decoded object
* @throws IOException if I/O error occurs
* @throws DecodeException if decoding fails
* @throws FeignException for HTTP-level errors
*/
Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
/**
* Default decoder that handles String, byte[], and provides 404 handling
*/
public static class Default implements Decoder {
/** Decode to String, byte[], or handle 404 responses */
public Object decode(Response response, Type type) throws IOException;
}
}
/**
* Exception thrown when decoding fails
*/
public class DecodeException extends FeignException {
public DecodeException(String message);
public DecodeException(String message, Throwable cause);
}Specialized decoder for handling HTTP error responses.
/**
* Decodes HTTP error responses into exceptions
*/
public interface ErrorDecoder {
/**
* Convert error response to exception
* @param methodKey Configuration key for the method
* @param response HTTP error response
* @return Exception to throw, or null to use default behavior
*/
Exception decode(String methodKey, Response response);
/**
* Default error decoder that creates FeignException for non-2xx responses
*/
public static class Default implements ErrorDecoder {
/** Create FeignException based on response status */
public Exception decode(String methodKey, Response response);
}
}Simple decoder for converting responses to strings.
/**
* Decoder that converts response bodies to strings
*/
public class StringDecoder implements Decoder {
/** Decode response body to string using response charset or UTF-8 */
public Object decode(Response response, Type type) throws IOException;
}import feign.Encoder;
import feign.RequestTemplate;
import java.lang.reflect.Type;
// Custom encoder for form data
public class FormEncoder implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (object instanceof Map) {
Map<String, Object> formData = (Map<String, Object>) object;
StringBuilder body = new StringBuilder();
for (Map.Entry<String, Object> entry : formData.entrySet()) {
if (body.length() > 0) {
body.append("&");
}
body.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(String.valueOf(entry.getValue()), "UTF-8"));
}
template.header("Content-Type", "application/x-www-form-urlencoded");
template.body(body.toString().getBytes("UTF-8"), Charset.forName("UTF-8"));
} else {
throw new EncodeException("FormEncoder only supports Map objects");
}
}
}
// Use custom encoder
API api = Feign.builder()
.encoder(new FormEncoder())
.target(API.class, "https://api.example.com");import feign.Decoder;
import feign.Response;
import java.lang.reflect.Type;
// Custom decoder for CSV data
public class CSVDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException {
if (type == List.class) {
List<Map<String, String>> records = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().asInputStream()))) {
String headerLine = reader.readLine();
String[] headers = headerLine.split(",");
String line;
while ((line = reader.readLine()) != null) {
String[] values = line.split(",");
Map<String, String> record = new HashMap<>();
for (int i = 0; i < headers.length && i < values.length; i++) {
record.put(headers[i].trim(), values[i].trim());
}
records.add(record);
}
}
return records;
}
throw new DecodeException("CSVDecoder only supports List<Map<String, String>>");
}
}
// Use custom decoder
API api = Feign.builder()
.decoder(new CSVDecoder())
.target(API.class, "https://api.example.com");import feign.ErrorDecoder;
import feign.Response;
import feign.gson.GsonDecoder;
// Custom error decoder that parses JSON error responses
public class CustomErrorDecoder implements ErrorDecoder {
private final Decoder decoder = new GsonDecoder();
@Override
public Exception decode(String methodKey, Response response) {
try {
if (response.status() >= 400) {
ErrorResponse errorResponse = (ErrorResponse) decoder.decode(response, ErrorResponse.class);
switch (response.status()) {
case 400:
return new BadRequestException(errorResponse.getMessage());
case 401:
return new UnauthorizedException(errorResponse.getMessage());
case 404:
return new NotFoundException(errorResponse.getMessage());
case 500:
return new ServerErrorException(errorResponse.getMessage());
default:
return new CustomAPIException(errorResponse.getMessage(), response.status());
}
}
} catch (Exception e) {
// Fall back to default behavior if parsing fails
}
// Return null to use default error handling
return null;
}
}
// Use custom error decoder
API api = Feign.builder()
.errorDecoder(new CustomErrorDecoder())
.target(API.class, "https://api.example.com");// Encoder that handles multiple formats
public class MultiFormatEncoder implements Encoder {
private final Encoder jsonEncoder = new GsonEncoder();
private final Encoder xmlEncoder = new JAXBEncoder();
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
Collection<String> contentType = template.headers().get("Content-Type");
if (contentType != null && contentType.contains("application/xml")) {
xmlEncoder.encode(object, bodyType, template);
} else {
// Default to JSON
template.header("Content-Type", "application/json");
jsonEncoder.encode(object, bodyType, template);
}
}
}
// Decoder that handles multiple formats
public class MultiFormatDecoder implements Decoder {
private final Decoder jsonDecoder = new GsonDecoder();
private final Decoder xmlDecoder = new JAXBDecoder();
@Override
public Object decode(Response response, Type type) throws IOException {
String contentType = response.headers().get("Content-Type").iterator().next();
if (contentType.contains("application/xml")) {
return xmlDecoder.decode(response, type);
} else {
return jsonDecoder.decode(response, type);
}
}
}// Decoder with gzip decompression
public class GzipDecoder implements Decoder {
private final Decoder delegate;
public GzipDecoder(Decoder delegate) {
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException {
String encoding = response.headers().get("Content-Encoding").iterator().next();
if ("gzip".equals(encoding)) {
// Create new response with decompressed body
Response decompressedResponse = response.toBuilder()
.body(new GZIPInputStream(response.body().asInputStream()), null)
.build();
return delegate.decode(decompressedResponse, type);
}
return delegate.decode(response, type);
}
}
// Use with compression
API api = Feign.builder()
.decoder(new GzipDecoder(new GsonDecoder()))
.requestInterceptor(template -> template.header("Accept-Encoding", "gzip"))
.target(API.class, "https://api.example.com");// Decoder with response caching
public class CachingDecoder implements Decoder {
private final Decoder delegate;
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public CachingDecoder(Decoder delegate) {
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException {
String cacheKey = response.request().url() + ":" + type.toString();
// Check cache for GET requests with 200 status
if ("GET".equals(response.request().method()) && response.status() == 200) {
Object cached = cache.get(cacheKey);
if (cached != null) {
return cached;
}
}
Object result = delegate.decode(response, type);
// Cache successful GET responses
if ("GET".equals(response.request().method()) && response.status() == 200) {
cache.put(cacheKey, result);
}
return result;
}
}// Default encoder handles String and byte[]
interface API {
@RequestLine("POST /data")
@Headers("Content-Type: text/plain")
void sendText(String text);
@RequestLine("POST /binary")
@Headers("Content-Type: application/octet-stream")
void sendBinary(byte[] data);
}import feign.codec.StringDecoder;
// Use string decoder for text responses
API api = Feign.builder()
.decoder(new StringDecoder())
.target(API.class, "https://api.example.com");
interface API {
@RequestLine("GET /text-data")
String getTextData();
}