Multi encoder - #3485
Conversation
Bumps [io.undertow:undertow-core](https://github.com/undertow-io/undertow) from 2.4.1.Final to 2.4.2.Final. - [Release notes](https://github.com/undertow-io/undertow/releases) - [Commits](undertow-io/undertow@2.4.1.Final...2.4.2.Final) --- updated-dependencies: - dependency-name: io.undertow:undertow-core dependency-version: 2.4.2.Final dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
…undertow-undertow-core-2.4.2.Final build(deps-dev): Bump io.undertow:undertow-core from 2.4.1.Final to 2.4.2.Final
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 6.1.0 to 6.1.1. - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](junit-team/junit-framework@r6.1.0...r6.1.1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
….junit-junit-bom-6.1.1 build(deps): Bump org.junit:junit-bom from 6.1.0 to 6.1.1
Bumps [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client) from 5.6.1 to 5.6.2. - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.6.2/RELEASE_NOTES.txt) - [Commits](apache/httpcomponents-client@rel/v5.6.1...rel/v5.6.2) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-version: 5.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [com.gradle:common-custom-user-data-maven-extension](https://github.com/gradle/common-custom-user-data-maven-extension) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/gradle/common-custom-user-data-maven-extension/releases) - [Commits](gradle/common-custom-user-data-maven-extension@v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: com.gradle:common-custom-user-data-maven-extension dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [com.google.http-client:google-http-client](https://github.com/googleapis/google-http-java-client) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](googleapis/google-http-java-client@v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: com.google.http-client:google-http-client dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
….gradle-common-custom-user-data-maven-extension-2.3.0 build(deps): Bump com.gradle:common-custom-user-data-maven-extension from 2.2.0 to 2.3.0
….apache.httpcomponents.client5-httpclient5-5.6.2 build(deps): Bump org.apache.httpcomponents.client5:httpclient5 from 5.6.1 to 5.6.2
….google.http-client-google-http-client-2.1.1 build(deps): Bump com.google.http-client:google-http-client from 2.1.0 to 2.1.1
Bumps [com.github.jknack:handlebars](https://github.com/jknack/handlebars.java) from 4.5.2 to 4.5.3. - [Release notes](https://github.com/jknack/handlebars.java/releases) - [Commits](jknack/handlebars.java@v4.5.2...v4.5.3) --- updated-dependencies: - dependency-name: com.github.jknack:handlebars dependency-version: 4.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps com.gradle:develocity-maven-extension from 2.4.2 to 2.5.0. --- updated-dependencies: - dependency-name: com.gradle:develocity-maven-extension dependency-version: 2.5.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
….gradle-develocity-maven-extension-2.5.0 build(deps-dev): Bump com.gradle:develocity-maven-extension from 2.4.2 to 2.5.0
….github.jknack-handlebars-4.5.3 build(deps): Bump com.github.jknack:handlebars from 4.5.2 to 4.5.3
Bumps [org.openrewrite.recipe:rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) from 3.38.0 to 3.39.0. - [Release notes](https://github.com/openrewrite/rewrite-migrate-java/releases) - [Commits](openrewrite/rewrite-migrate-java@v3.38.0...v3.39.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-migrate-java dependency-version: 3.39.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [org.openrewrite.maven:rewrite-maven-plugin](https://github.com/openrewrite/rewrite-maven-plugin) from 6.42.0 to 6.43.0. - [Release notes](https://github.com/openrewrite/rewrite-maven-plugin/releases) - [Commits](openrewrite/rewrite-maven-plugin@v6.42.0...v6.43.0) --- updated-dependencies: - dependency-name: org.openrewrite.maven:rewrite-maven-plugin dependency-version: 6.43.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
….openrewrite.recipe-rewrite-migrate-java-3.39.0 build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java from 3.38.0 to 3.39.0
….openrewrite.maven-rewrite-maven-plugin-6.43.0 build(deps-dev): Bump org.openrewrite.maven:rewrite-maven-plugin from 6.42.0 to 6.43.0
Bumps [org.openrewrite.recipe:rewrite-testing-frameworks](https://github.com/openrewrite/rewrite-testing-frameworks) from 3.40.0 to 3.41.0. - [Release notes](https://github.com/openrewrite/rewrite-testing-frameworks/releases) - [Commits](openrewrite/rewrite-testing-frameworks@v3.40.0...v3.41.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-testing-frameworks dependency-version: 3.41.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
….openrewrite.recipe-rewrite-testing-frameworks-3.41.0 build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks from 3.40.0 to 3.41.0
Bumps `netty.version` from 4.2.15.Final to 4.2.16.Final. Updates `io.netty:netty-bom` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](netty/netty@netty-4.2.15.Final...netty-4.2.16.Final) Updates `io.netty:netty-handler` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](netty/netty@netty-4.2.15.Final...netty-4.2.16.Final) Updates `io.netty:netty-codec-http` from 4.2.15.Final to 4.2.16.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](netty/netty@netty-4.2.15.Final...netty-4.2.16.Final) --- updated-dependencies: - dependency-name: io.netty:netty-bom dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.16.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
…ty.version-4.2.16.Final build(deps): Bump netty.version from 4.2.15.Final to 4.2.16.Final
Bumps `jackson.version` from 2.22.0 to 2.22.1. Updates `com.fasterxml.jackson:jackson-bom` from 2.22.0 to 2.22.1 - [Commits](FasterXML/jackson-bom@jackson-bom-2.22.0...jackson-bom-2.22.1) Updates `com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider` from 2.22.0 to 2.22.1 --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-version: 2.22.1 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider dependency-version: 2.22.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps `vertx.version` from 5.1.3 to 5.1.4. Updates `io.vertx:vertx-junit5` from 5.1.3 to 5.1.4 - [Commits](eclipse-vertx/vertx-junit5@5.1.3...5.1.4) Updates `io.vertx:vertx-web-client` from 5.1.3 to 5.1.4 - [Commits](vert-x3/vertx-web@5.1.3...5.1.4) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 5.1.4 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 5.1.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
…tx/feign-vertx5-test/vertx.version-5.1.4 build(deps-dev): Bump vertx.version from 5.1.3 to 5.1.4 in /vertx/feign-vertx5-test
…kson.version-2.22.1 build(deps): Bump jackson.version from 2.22.0 to 2.22.1
Bumps `vertx.version` from 4.5.28 to 4.5.29. Updates `io.vertx:vertx-junit5` from 4.5.28 to 4.5.29 - [Commits](eclipse-vertx/vertx-junit5@4.5.28...4.5.29) Updates `io.vertx:vertx-web-client` from 4.5.28 to 4.5.29 - [Commits](vert-x3/vertx-web@4.5.28...4.5.29) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 4.5.29 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 4.5.29 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
…tx/feign-vertx4-test/vertx.version-4.5.29 build(deps-dev): Bump vertx.version from 4.5.28 to 4.5.29 in /vertx/feign-vertx4-test
Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.2.0 to 3.2.1. - [Commits](FasterXML/jackson-bom@jackson-bom-3.2.0...jackson-bom-3.2.1) --- updated-dependencies: - dependency-name: tools.jackson:jackson-bom dependency-version: 3.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 6.1.1 to 6.1.2. - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](junit-team/junit-framework@r6.1.1...r6.1.2) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-version: 6.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
|
@yvasyliev I have finished the refactor. I hope the surgery wasn't too bad. Biggest change was renaming DelegateEncoder to MultiEncoder and moving that to the core module. Please provide feedback when you get a chance! Also, if you are ok with these changes, it would probably be a good idea to withdraw #3476 so the maintainers don't get confused. As a side note, I'm not completely sure where the core/api split is headed, but I would normally expect api to have only interfaces and exceptions (not all the concrete classes they have in the feign package). If we do wind up wanting to adjust |
|
I maintain my comment on #3476 (comment) Regarding the format this should take, I think the MultiEncoder needs a predicate and encore |
|
I don't think we can mix static and non-static add methods... if that is the case,
or
with each add returning a new immutable instance |
|
that could target main, and it would be amazing to have decoder following the same principle |
yvasyliev
left a comment
There was a problem hiding this comment.
Thanks @trumpetinc for driving this change! I only have one question - is it required to bump maven dependencies across multiple modules in the scope of this PR?
All my review comments are really minor, so feel free to ignore them. I still think it would be cleaner for Encoder to have separate canEncode and encode method because that's how I prefer to structure code, but I don't have any architectural reasoning to push for it. It would be great to get this PR merged as-is, though!
There was a problem hiding this comment.
Why not leverage the full strength of MockitoExtension? 😊
Besides, it's better to have each test method self-contained, meaning that object, bodyType & requestTemplate must be local variables. You never know when we will need to add a test case with non-default method arguments (e.g., nulls)
@ExtendWith(MockitoExtension.class)
class PredicateEncoderTest {
@Mock private Encoder delegate;
@Mock private EncoderPredicate encoderPredicate
@InjectMocks private PredicateEncoder predicateEncoder;
@Test
void shouldEncode() {
var object = "Hello, World!";
var bodyType = String.class;
var requestTemplate = mock(RequestTemplate.class);
when(encoderPredicate.test(object, bodyType, requestTemplate)).thenReturn(true);
when(delegate.encode(object, bodyType, requestTemplate)).thenReturn(true);
assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isTrue();
verify(delegate).encode(object, bodyType, requestTemplate);
}
@Test
void shouldNotEncode() {
var object = "Hello, World!";
var bodyType = String.class;
var requestTemplate = mock(RequestTemplate.class);
assertThat(predicateEncoder.encode(object, bodyType, requestTemplate)).isFalse();
verifyNoInteractions(delegate);
}
}There was a problem hiding this comment.
That works for me - I also added a when()....thenReturn(false) to the encoderPredicate to make it clear that we are explicitly returning false instead of relying on default mock behavior).
I definitely learned something from your example - thank you.
It's a breaking change @velo |
it doesn't have to be... why must be a breaking change? |
@velo can you help me understand the advantages of this over having a PredicateEncoder that wraps other encoders? I feel like PredicateEncoder (included in this PR) is a better separation of concerns than making the multi-encoder itself hard-coded with predicate awareness. I'm also pretty sure that predicates will need to handle 3 arguments (same as the encode() method). Request does not have type(), contentType() or length() members. We could certainly have static accessors for common types of predicates to make it easier (for example, I think a PredicateEncoder.forJsonContentType(delegate) makes sense): or, if we are ok with expanding the arguments of BaseBuilder.encoder to accept a vararg, then it becomes: |
@velo can you please expand on this feedback? I wasn't intending to have add methods at all... I think it is pretty unlikely that a user will want to add encoders to an existing MultiEncoder (they can always wrap a MultiEncoder in another MultiEncoder if that becomes necessary). So a single static factory method with a vararg makes for really clean code. Also, there is no concept of a 'default' encoder. The encoders are processed linearly until one is found that can handle the request. In the unusual case where someone wants to compose an existing MultiEncoder with another encoder, it would look like this: I think that this approach allows us to work with the Encoder interface as much as possible (Liskov Substitution Principle). |
|
@velo one thing that I wanted to ask about: Can you give some history on the core->api module split? There is a ton of code in the api module that was moved from the core module (and it is not really 'api-like'). The Feign builder interface, for example, is now in api. This means that if we want to do anything in the builder, we are forced to move more core classes into the api module. Specific example: I would really like to change the to The builder would wrap the varargs in a MultiEncoder behind the scenes. But this isn't currently possible without moving MultiEncoder from core to api. It just feels very weird to me to have an api module that still has a ton of core functionality code in it... So I feel like I must be missing something about the intent of api vs core. Thx |
The semantics of the Encoder interface must change to support this. The current encode() method uses a thrown EncodeException for two scenarios: a) Encoder cannot handle the request; and b) An error happened during encoding that the Encoder should have been able to handle. We need to separate these two code flows, which means a semantic change. A change in semantics of a public API, unfortunately, absolutely mandates a breaking change. In this case, I have decided to force implementers to respond to the change in semantics by changing the method signature of
It seems like such a small thing, but semantic changes of public APIs are really tricky - so super important that they be very obviously breaking changes. |
@velo I actually looked at Decoder yesterday. It will probably be less impactful than on the encoder side, but I would prefer to use the same syntax for configuring encoders and decoders, so I think it is worth consideration. The question becomes: "How do we differentiate between the 'decoder does not handle this return type' case vs the 'decoding error was encountered' case". Here are my initial thoughts (if this looks promising, we should probably break this out into a separate PR/issue): decode() has a return type. We could have decode() return a sentinel value (Decoder.NOT_SUPPORTED) to indicate that the type isn't supported. One issue I see here is that this will need to be a semantic change, but we don't have a clean way of forcing implementers to rewrite their code. This could result in really ugly runtime failures after a user migrates. I'm not sure what to do about that - normally this would be done by changing the method signature in a breaking way, but it's not obvious how to do that here. Do you have any thoughts? Once we have the semantic issue figured out, creating a MultiDecoder and PredicateDecoder will be simple. |
|
@yvasyliev @velo one question: I was trying to keep the scope of this PR small (as small as possible, anyway), so I did not include any of the methods/classes related to creating EncoderPredicate of various types. My intent is to add those in a future PR - is that OK, or would it be better to include those now? Basically, I really, really want to get this PR accepted and merged so other dependent PRs (forms overhaul for example) can proceed. |
😊 I apologize for moving conversations in circles. May I ask you to revisit the There's a broken single responsibility principle in Feign's If it were a pure encoder, the Just a side noteIf we need to reduce the number of method parameters, we can pack everything into a container: To be explicit, I don't mind having a mutating The only sentinel value I'm aware of in Java is The alternative is to have explicit |
This syntax looks nice.... but would need to be the other way around with major gain in terms of functionality, and don't need to break compatibility love the recommended Predicates, just make sure PredicatedEncoder is a functionaly type so people can easily expand on this I still see value on a easy syntax to provide PredicatedEncoder/Encoder pairs to build a MultiEncoder/Decoder There is a lot happening, I can't keep up |
|
Do see an issue (backwards compatibility, etc...) with just changing the This change is source compatible with previous code. This change is not compile time compatible, but we are already making breaking changes because of the semantic change, so breaking changes are absolutely mandatory - no way around that. So unless I am missing something, making a source compatible change should be fine to do. I also do not want to assume that users will want to always add PredicateEncoder as their additional encoders - that class is very useful for some scenarios, but certainly not all. |
|
@yvasyliev I am fine with discussing this until we get it right. I have had a very long day today and tomorrow, so it will be a couple of days before I can respond. Apologies for the delay! |
|
@yvasyliev finally had a chance to read your response ( #3485 (comment) ) and give it some careful thought. Attempting to find symmetry between Encoder and DecoderFor background, during one of my earlier attempts at adding streaming support to Feign, I actually did a lot of work attempting to unify the concepts of a request body and response body. The understanding I gained from that effort is informing my opinions on this (including the fact that trying to unify request and response bodies was a terrible idea!). Here are my opinions:
Decoder produces a Java object (return type of the service interface method), and that's pretty much it. Encoder is a completely different animal. It is not just about taking a Java object and converting it into a stream of bytes that are sent to the server. Having the ability to transform the RequestTemplate is fundamental to the Feign encoding lifecycle. In fact, it is helpful to think about RequestTemplate as being a builder interface (mutable), and Encoder.encode() is intended to make changes to that builder. Based on what I've seen in the architecture and code, the Feign designers almost certainly intended for the Encoder to be able to manipulate the request body and the request headers and other aspects of the RequestTemplate as needed. I probably wouldn't have designed it this way myself - but there are advantages to the current approach. I am not completely opposed to attempting to change this (i.e. make the encoder so it focuses only on the bodies) - but I think this would be a massive, massive challenge with all sorts of unexpected fallout. How to indicate that a Decoder did not work(I'm sure you guys are sick of hearing me talk about semantic differences! But I'm really serious that needs to be taken into account when we make breaking changes... I've lost months and a ton of money when vendors made semantic changes that didn't force compile time errors, so I am coming at this from a place of a lot of painful experience). I see six approaches (I realize I'm partially restating what yvasyliev wrote - but I want to summarize all options) for dealing with this semantic change:
Each of the above has pros/cons: I think that narrowing this list down will require determining whether we care about "Runtime errors for callers". If this is something we have to avoid at all costs, then the exception and change method name may be the only ways to do this. Personally, I think accepting the risk of runtime exceptions for callers is an acceptable compromise. These methods truly have no value except by Feign core (maybe in some unit test scenarios, but runtime errors are fine in a test situation). A comment about separation of concernsI think that a lot of your view on separation of concerns is being driven by the idea that Encoder's concern is to take a request body argument from the method call and convert it into a request Body object. I do not view that as Encoder's concern. I view that Encoder's concern is "Manipulate a RequestTemplate to ensure that it is ready to be sent to the server". In that context, the boolean return type is not a matter of "can the encoder handle the request?", it is "did the encoder actually handle the request?" This is a subtle thing, but is why I believe that canEncode() isn't appropriate. Fundamentally, in a chain of responsibility design, the goal is to keep trying until you have success - then stop. I could be convinced of the need for a canEncode method - but only if there were use cases where that method would be called by itself. In the context of the Feign request processing cycle, every single call to canEncode() would be paired with a subsequent call to encode(). This is a clear sign that the purpose of the returned boolean is not to provide information about whether an Encoder can encode (along with weird side effects of actually doing the encoding!). The purpose of the returned boolean is to inform the caller that the RequestTemplate was actually updated - thus telling the chain of responsibility that it's work is done. I hope that makes sense. And for sure, if you have an example of where Encoder.canEncode() would be called standalone for a reason that is truly answering the question ("Can you encode this request?"), I am very happy to consider it. I just don't see it in a chain of responsibility design like we have here. Maybe have an EncodeResult?If we wanted to preserve symmetry between Encoder.encode() and Decoder.decode(), we could go with having them return an EncodeResult / DecodeResult. The EncodeResult would pretty much just have an encodingWasPerformed boolean - so not much advantage - but it would allow for some cosmetic symmetry between the calls. Side Note: EncodeResult can not include a summary of the changes made to the RequestTemplate, unless we are going to try to make the RequestTemplate passed to the encode() method immutable, then introduce a RequestTemplateMutator that the encode() method returns and is then applied to the RequestTemplate. This just feels like a huge undertaking for pretty minimal gains. Would a simple EncodeResult like I'm describing help with "it's not symmetrical" feeling a bit? One thing that I like this approach is that the method names would make really clear what was intended. Returning 'true' or 'false' is not nearly as informative as returning EncodeResult.encodingSuccessful() vs EncodeResult.encodingNotSupported(). |
we can control support in the predicate, so encoder stays as is. |
|
The current Encoder.encode() method uses a thrown EncodeException to indicate TWO outcomes: a) Encoder does not support encoding of the request; and b) an error happened during encoding. This means that callers have no way of knowing whether an exception thrown by encode() means failure, or just that the Encoder doesn't support the operation. It is not possible to construct a MultiEncoder with that ambiguity. If you feel there is, we will need to see some code showing how it would work. I think that we need to get clear on this before we have any discussion about adding this to a BaseBuilder.encode() method. |
Continuing work started in: #3476
Co-authored-by: trumpetinc 6618744+trumpetinc@users.noreply.github.com and yvasyliev ye.vasyliev@gmail.com
Note
Target branch:
14.xImportant
Please squash and merge this PR if it's approved.
Important
Please add
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> and yvasyliev <ye.vasyliev@gmail.com>footer to the commit message if this PR is approved.Summary
This PR introduces multi-encoder support in Feign, allowing users to register multiple encoders that are automatically selected at request time. The
Encoder.encode()method now returnsboolean(trueif the encoder handled the object,falseotherwise), and a newEncoder.of()factory combines encoders into aDelegatingEncoder. EncoderException is now thrown only when there are true encoding exceptions.Motivation
Previously, Feign only supported a single
Encoderregistered viaFeign.builder().encoder(...). For APIs that mix different content types (JSON, XML, form data, etc.), users had to manually wrap encoders (e.g.,new FormEncoder(new JacksonEncoder())). This was brittle and didn't scale well.Changes
New API
Encoder.encode()now returnsboolean- returntruewhen encoding succeeds,falseif the encoder does not handle the request. This replaces the need for a separatecanEncode()method. Note that this is a semantic change of the encode() method - previously, an EncodeException was thrown in two scenarios: the encoder could not handle the encoding request and when there was a failure in handling the encoding request. Now, a return type of 'false' is used to indicate that the encoder could not handle the encoding request. EncodeException throw is reserved for true failures to encode.MultiEncoder- a composite encoder that delegates to a list of encoders, using the first one whoseencode()returnstrue. If none of the delegates can handle the request, MultiEncoder.encode() returns false.PredicateEncoder- Conditionally delegates to a delegate encoder if the provided EnocerPredicate passes. Useful for conditionally processing a given encoder (for example, if the user wishes to limit JsonEncoder based on a content-type header value)Encoder Updates
All built-in encoders now return
booleanfromencode()and gate onContent-Typewhere applicable:DefaultEncoderString,byte[],File,Path,InputStream,Request.Body, andnullFormEncodermultipart/form-dataandapplication/x-www-form-urlencodedGraphqlEncoderGraphqlContract; delegates to wrapped encoder otherwiseMeteredEncoder(all variants)Example Usage
Before:
After:
Breaking Changes
Breaking changes are documented in
MIGRATION-v14.md(sections 14–17):Encoder.encode()now returnsbooleaninstead ofvoid. All customEncoderimplementations must update the return type and returntrueon success orfalsewhen the encoder can not handle the request (instead of throwing EncodeException). EncodeException should still be thrown for encoding failures.Feign.builder().encoder(new JacksonEncoder())still works.