Wire-contract parity: serializer, date split, payment_details discriminator, and error mapping - #218
Draft
tomas-amaro wants to merge 4 commits into
Draft
Wire-contract parity: serializer, date split, payment_details discriminator, and error mapping#218tomas-amaro wants to merge 4 commits into
tomas-amaro wants to merge 4 commits into
Conversation
…rence SDK
Five wire-contract parity defects from docs/parity/FLEET.md sections 4 and 6,
all verified against the corpus in docs/flows/ and the C# reference at
sdk_net@9165cf5.
1. dropoff_latitiude. RideLineItem.dropoffLatitude had no @SerializedName, so
Gson's LOWER_CASE_WITH_UNDERSCORES derived dropoff_latitude. The live wire
name is the transposed dropoff_latitiude (RideTicketLineItem.cs:101) and the
API expects it; the "corrected" spelling silently dropped ride dropoff
geolocation with no error. Pinned explicitly, with a comment and a test that
assert the misspelling so it is not "fixed" again. The other 19 RideLineItem
fields derive correctly and are now locked by a test.
2. HMAC key charset. SHA256Handler used authKey.getBytes(), the platform
default, making the signature depend on a JVM locale setting. Pinned to
UTF-8, which matches the reference's ASCII byte for byte for the hex tokens
Riskified issues. The corpus reference vector is now asserted.
3. The date split. The contract splits 25 offset-bearing fields from 13 naive
ones, following no principle. One global Date adapter cannot express that, so
the 13 naive fields carry @JsonAdapter(NaiveDateTypeAdapter.class), which
Gson gives precedence over the registered adapter; everything else keeps the
offset format. Both formats now render in UTC with Locale.US, so the same
Date no longer serializes differently on two machines -- and neither does the
HMAC computed over those bytes.
4. The method discriminator. payment_details carries no type discriminator on
the wire; the variant is expressed by which keys are present, with a constant
payment_type as the de-facto discriminator. The RuntimeTypeAdapterFactory
injected a "method" key whose values disagreed with the payment_type emitted
alongside. Replaced with PaymentDetailsAdapterFactory, which dispatches on
runtime type and emits no label. The replacement is necessary rather than
cosmetic: BaseOrder and DecisionOrder declare the field as
List<? extends IPaymentDetails>, and Gson's built-in runtime-type promotion
fires only for a Class element type, not a WildcardType -- simply
unregistering the old factory serializes every element as {}.
StripePaymentDetails is left in place, minus the method key.
5. Error mapping. The default: branch rewrote every unmatched status to
500 "Contact Riskified support" and discarded the body, so a 502, a 503 and a
genuine 500 were indistinguishable. Statuses are now reported as themselves
through RiskifiedHttpException, which extends HttpResponseException (so
existing catch blocks and getStatusCode() are unaffected) and carries
statusText, the raw body, and the parsed error as separate structured fields
that logging can redact. The checkout path no longer dereferences
getError().getMessage() before establishing that the body parsed as that
shape: six of the seven documented error shapes used to raise a
NullPointerException in place of the HTTP error.
Deliberately unchanged: null-handling and always-emitted-default behaviour
(corpus open question 5), the Accept header, User-Agent, the Version header, and
the webhook receiver. A before/after payload diff confirms the only key-set
changes are the dropoff_latitiude rename and the removal of method.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ody charset Three follow-up divergences found while doing the transport tranche, approved for fixing after review. Same guardrails: null-handling and the 30 always-emitted defaults are untouched (corpus open question 5). 1. KycDetails.updateAt derived the wire key "update_at"; the contract key is "updated_at" (KycDetails.cs:18). The derived name was a key the API ignores, so KYC update timestamps were silently not arriving -- the same class of failure as dropoff_latitiude, in the opposite direction: there the SDK "corrected" a wire typo, here it inherited a Java-side one. Pinned with @SerializedName rather than renaming the field, which would break every caller of getUpdateAt()/setUpdateAt(). 2. Added Customer.verifiedEmailAt and Customer.firstPurchaseAt, absent from the Java model entirely (Customer.cs:143, :167). Both are DateTimeOffset? in the reference, so they are boxed Date here: omitted when null, offset-bearing by default, no @JsonAdapter. The date split now covers 25/25 offset-bearing fields rather than 23/25. A before/after probe confirms the payload for a default Customer is byte-identical. 3. postOrder read the response body with EntityUtils.toString(entity) and no charset. The fallback is subtler than it first looks -- for application/json httpclient resolves UTF-8 from ContentType.APPLICATION_JSON's registered default -- but it is ISO-8859-1 for text/html, text/plain, and for a body with no Content-Type at all, which is exactly the 502/503-from-a-proxy and bare-string-OTP-error case the unmatched-status work is about. Now passes "UTF-8" explicitly, matching postCheckoutOrder, so the body preserved on RiskifiedHttpException is not mojibake. 4. Response.getReceived() unboxed a null Integer, raising NullPointerException on every response that omits "received" -- which is every error response. It now returns 0, keeping the primitive return type so existing callers still compile, and getReceivedOrNull() exposes the absent/zero distinction. The CheckoutResponse copy constructor uses the latter so absence survives. Also audited all 28 non-derivable wire names in 01-model-catalog.md section 5 against their Java fields. 26 are correct; the other two -- group_founder_order_id (OrderBase.cs:188) and buyer_anonymous_id (Customer.cs:155) -- are not modelled in Java at all. Reported, not fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…osed override
Reverts the `@SerializedName("dropoff_latitiude")` pin added earlier on this
branch, and inverts the test that asserted it.
The pin was based on the contract corpus, which claimed the transposed
`dropoff_latitiude` was the live wire name because the reference C# SDK sends
it. That inference was wrong:
* Both OpenAPI specs declare `dropoff_latitude`, with a description and an
example. The transposed spelling appears in neither.
* It occurs exactly once in all of sdk_net
(Riskified.SDK/Model/OrderElements/RideTicketLineItem.cs:100), with no test
covering it, while pickup_latitude (:89) and dropoff_longitude (:104) in the
same class are spelled correctly.
So Gson's derived `dropoff_latitude` was right all along, and this branch was
about to break a field that already worked. The transposition is a defect in the
C# SDK.
* RideLineItem: override and its DO-NOT-FIX comment removed; the now-unused
SerializedName import goes with it. The field derives to the contract name.
* RideLineItemTest: asserts `dropoff_latitude` is emitted and
`dropoff_latitiude` is not -- guarded in both directions, since this branch
already pinned it the wrong way once.
* The wire-name map test drops the transposed entry.
* KycDetailsTest: javadoc no longer cites the typo as a contract quirk.
The corpus has been corrected in Riskified/sdk-orchestrator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Wire-contract parity fixes for the Java SDK, derived from the language-neutral
Riskified contract corpus and audited against the C# reference implementation
(
Riskified/sdk_net@9165cf5). Two commits, production source plus regressiontests.
Changes
Serializer parity
dropoff_latitude—RideLineItem.dropoffLatitudehas no@SerializedName, so Gson derivesdropoff_latitude, which is correct: both OpenAPI specs declare that spelling, with a description and an example. An earlier revision of this branch pinned the transposeddropoff_latitiudeinstead, on the strength of the contract corpus — which had inferred the misspelling was the live wire name purely from the fact that the C# SDK sends it (RideTicketLineItem.cs:100). That inference was wrong, and the pin has been reverted: the transposed spelling appears in neither spec, occurs exactly once in all ofsdk_netwith no test covering it, and sits beside a correctly spelledpickup_latitudeanddropoff_longitudein the same class. It is a defect in the C# SDK, not the contract. The test now assertsdropoff_latitudeis emitted anddropoff_latitiudeis not, guarded in both directions. The other 19RideLineItemfields derive correctly and are locked by a test. The corpus has been corrected inRiskified/sdk-orchestrator.SHA256HandlerusedauthKey.getBytes(), the platform default, making the signature depend on a JVM locale setting. Pinned to UTF-8.@JsonAdapter(NaiveDateTypeAdapter.class); everything else keeps the offset format. Both formats render in UTC withLocale.US, so the sameDateno longer serializes differently on two machines — and neither does the HMAC computed over those bytes.methoddiscriminator —payment_detailscarries no type discriminator on the wire; the variant is expressed by which keys are present.RuntimeTypeAdapterFactoryinjected amethodkey whose values disagreed with thepayment_typeemitted alongside. Replaced withPaymentDetailsAdapterFactory, which dispatches on runtime type and emits no label. The replacement is necessary rather than cosmetic:BaseOrder/DecisionOrderdeclare the field asList<? extends IPaymentDetails>, and Gson's runtime-type promotion fires only for aClasselement type, not aWildcardType— simply unregistering the old factory serializes every element as{}.KycDetails.updateAtderivedupdate_at; the contract key isupdated_at(KycDetails.cs:18), so KYC update timestamps were silently not arriving. Pinned with@SerializedNamerather than renaming the field, which would break every caller ofgetUpdateAt()/setUpdateAt().Customer.verifiedEmailAt/Customer.firstPurchaseAtwere absent from the Java model entirely (Customer.cs:143,:167). Added as offset-bearing boxedDate, omitted when null. The date split now covers 25/25 offset-bearing fields.Error mapping
default:branch rewrote every unmatched status to500 "Contact Riskified support"and discarded the body, so a 502, a 503 and a genuine 500 were indistinguishable. Statuses are now reported as themselves through a newRiskifiedHttpException, which extendsHttpResponseException— existingcatchblocks andgetStatusCode()are unaffected — and carriesstatusText, the raw body, and the parsed error as separate structured fields that logging can redact.getError().getMessage()before establishing that the body parsed as that shape. Six of the seven documented error shapes previously raised aNullPointerExceptionin place of the HTTP error.postOrderread the response body with no charset. httpclient resolves UTF-8 forapplication/json, but ISO-8859-1 fortext/html,text/plain, and a body with noContent-Typeat all — exactly the 502/503-from-a-proxy case above. Now passes UTF-8 explicitly, matchingpostCheckoutOrder.Response.getReceived()unboxed a nullInteger, raisingNullPointerExceptionon every response that omitsreceived— which is every error response. It now returns 0, keeping the primitive return type so existing callers still compile;getReceivedOrNull()exposes the absent/zero distinction.Deliberately unchanged
Null-handling and the 30 always-emitted defaults (open question in the corpus), the
Acceptheader,User-Agent, theVersionheader, and the webhook receiver.StripePaymentDetailsstays in place, minus themethodkey.A before/after payload diff confirms the only key-set changes are the
dropoff_latitiuderename and the removal ofmethod.Reviewer notes
getReceived()returning 0, unmatched statuses no longer collapsing to 500) are called out above.group_founder_order_id(OrderBase.cs:188) andbuyer_anonymous_id(Customer.cs:155) — are not modelled in Java at all. Reported here, not fixed.d02605fand is one commit behindmaster(fa18fe0). It merges cleanly.🤖 Generated with Claude Code