ExternalTypeCustomResolverTest.java
package com.fasterxml.jackson.databind.jsontype.ext;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SuppressWarnings("hiding")
public class ExternalTypeCustomResolverTest extends DatabindTestUtil
{
// [databind#1288]
public static class ClassesWithoutBuilder {
public static class CreditCardDetails implements PaymentDetails {
protected String cardHolderFirstName;
protected String cardHolderLastName;
protected String number;
protected String expiryDate;
protected int csc;
protected String address;
protected String zipCode;
protected String city;
protected String province;
protected String countryCode;
protected String description;
public void setCardHolderFirstName(String cardHolderFirstName) {
this.cardHolderFirstName = cardHolderFirstName;
}
public void setCardHolderLastName(String cardHolderLastName) {
this.cardHolderLastName = cardHolderLastName;
}
public void setNumber(String number) {
this.number = number;
}
public void setExpiryDate(String expiryDate) {
this.expiryDate = expiryDate;
}
public void setCsc(int csc) {
this.csc = csc;
}
public void setAddress(String address) {
this.address = address;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
public void setCity(String city) {
this.city = city;
}
public void setProvince(String province) {
this.province = province;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
public void setDescription(String description) {
this.description = description;
}
}
public static class EncryptedCreditCardDetails implements PaymentDetails {
protected UUID paymentInstrumentID;
protected String name;
public void setPaymentInstrumentID(UUID paymentInstrumentID) {
this.paymentInstrumentID = paymentInstrumentID;
}
public void setName (String name) {
this.name = name;
}
}
public enum FormOfPayment {
INDIVIDUAL_CREDIT_CARD (CreditCardDetails.class), COMPANY_CREDIT_CARD (
CreditCardDetails.class), INSTRUMENTED_CREDIT_CARD (EncryptedCreditCardDetails.class);
private final Class<? extends PaymentDetails> clazz;
FormOfPayment(final Class<? extends PaymentDetails> clazz) {
this.clazz = clazz;
}
@SuppressWarnings("unchecked")
public <T extends PaymentDetails> Class<T> getDetailsClass () {
return (Class<T>) this.clazz;
}
public static FormOfPayment fromDetailsClass(Class<PaymentDetails> detailsClass) {
for (FormOfPayment fop : FormOfPayment.values ()) {
if (fop.clazz == detailsClass) {
return fop;
}
}
throw new IllegalArgumentException("not found");
}
}
public interface PaymentDetails {
public interface Builder {
PaymentDetails build();
}
}
public static class PaymentMean {
FormOfPayment formOfPayment;
PaymentDetails paymentDetails;
public void setFormOfPayment(FormOfPayment formOfPayment) {
this.formOfPayment = formOfPayment;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "form_of_payment", visible = true)
@JsonTypeIdResolver(PaymentDetailsTypeIdResolver.class)
public void setPaymentDetails(PaymentDetails paymentDetails) {
this.paymentDetails = paymentDetails;
}
}
public static class PaymentDetailsTypeIdResolver extends TypeIdResolverBase {
@SuppressWarnings("unchecked")
@Override
public String idFromValue (Object value) {
if (! (value instanceof PaymentDetails)) {
return null;
}
return FormOfPayment.fromDetailsClass ((Class<PaymentDetails>) value.getClass ()).name ();
}
@Override
public String idFromValueAndType (Object value, Class<?> suggestedType) {
return this.idFromValue (value);
}
@Override
public JavaType typeFromId (DatabindContext context, String id) {
return context.getTypeFactory().constructType(FormOfPayment.valueOf(id).getDetailsClass ());
}
@Override
public String getDescForKnownTypeIds () {
return "PaymentDetails";
}
@Override
public Id getMechanism () {
return JsonTypeInfo.Id.CUSTOM;
}
}
}
public static class ClassesWithBuilder {
@JsonDeserialize (builder = CreditCardDetails.IndividualCreditCardDetailsBuilder.class)
public static class CreditCardDetails implements PaymentDetails {
@JsonPOJOBuilder(withPrefix = "")
public static class CompanyCreditCardDetailsBuilder implements Builder {
private String cardHolderFirstName;
private String cardHolderLastName;
private String number;
private int csc;
@Override
public CreditCardDetails build() {
return new CreditCardDetails (cardHolderFirstName, cardHolderLastName, number, csc,
"COMPANY CREDIT CARD");
}
public CompanyCreditCardDetailsBuilder cardHolderFirstName(final String cardHolderFirstName) {
this.cardHolderFirstName = cardHolderFirstName;
return this;
}
public CompanyCreditCardDetailsBuilder cardHolderLastName(final String cardHolderLastName) {
this.cardHolderLastName = cardHolderLastName;
return this;
}
public CompanyCreditCardDetailsBuilder csc(final int csc) {
this.csc = csc;
return this;
}
public CompanyCreditCardDetailsBuilder number(final String number) {
this.number = number;
return this;
}
}
@JsonPOJOBuilder (withPrefix = "")
public static class IndividualCreditCardDetailsBuilder implements Builder {
private String cardHolderFirstName;
private String cardHolderLastName;
private String number;
private int csc;
private String description;
@Override
public CreditCardDetails build () {
return new CreditCardDetails(cardHolderFirstName, cardHolderLastName, number, csc,
description);
}
public IndividualCreditCardDetailsBuilder cardHolderFirstName(final String cardHolderFirstName) {
this.cardHolderFirstName = cardHolderFirstName;
return this;
}
public IndividualCreditCardDetailsBuilder cardHolderLastName(final String cardHolderLastName) {
this.cardHolderLastName = cardHolderLastName;
return this;
}
public IndividualCreditCardDetailsBuilder csc (final int csc) {
this.csc = csc;
return this;
}
public IndividualCreditCardDetailsBuilder description (final String description) {
this.description = description;
return this;
}
public IndividualCreditCardDetailsBuilder number (final String number) {
this.number = number;
return this;
}
}
protected final String cardHolderFirstName;
protected final String cardHolderLastName;
protected final String number;
protected final int csc;
protected final String description;
public CreditCardDetails (final String cardHolderFirstName, final String cardHolderLastName,
final String number, final int csc,
final String description) {
super();
this.cardHolderFirstName = cardHolderFirstName;
this.cardHolderLastName = cardHolderLastName;
this.number = number;
this.csc = csc;
this.description = description;
}
}
@JsonDeserialize (builder = EncryptedCreditCardDetails.InstrumentedCreditCardBuilder.class)
public static class EncryptedCreditCardDetails implements PaymentDetails {
@JsonPOJOBuilder (withPrefix = "")
public static class InstrumentedCreditCardBuilder implements Builder {
private UUID paymentInstrumentID;
private String name;
@Override
public EncryptedCreditCardDetails build () {
return new EncryptedCreditCardDetails (this.paymentInstrumentID, this.name);
}
public InstrumentedCreditCardBuilder name (final String name) {
this.name = name;
return this;
}
public InstrumentedCreditCardBuilder paymentInstrumentID (final UUID paymentInstrumentID) {
this.paymentInstrumentID = paymentInstrumentID;
return this;
}
}
protected final UUID paymentInstrumentID;
protected final String name;
EncryptedCreditCardDetails (final UUID paymentInstrumentID, final String name) {
super();
this.paymentInstrumentID = paymentInstrumentID;
this.name = name;
}
}
public enum FormOfPayment {
INDIVIDUAL_CREDIT_CARD (CreditCardDetails.IndividualCreditCardDetailsBuilder.class), COMPANY_CREDIT_CARD (
CreditCardDetails.CompanyCreditCardDetailsBuilder.class), INSTRUMENTED_CREDIT_CARD (EncryptedCreditCardDetails.InstrumentedCreditCardBuilder.class);
private final Class<? extends PaymentDetails.Builder> builderClass;
FormOfPayment(final Class<? extends PaymentDetails.Builder> builderClass) {
this.builderClass = builderClass;
}
@SuppressWarnings ("unchecked")
public <T extends PaymentDetails> Class<T> getDetailsClass() {
return (Class<T>) this.builderClass.getEnclosingClass();
}
public static FormOfPayment fromDetailsClass(Class<PaymentDetails> detailsClass) {
for (FormOfPayment fop : FormOfPayment.values()) {
if (fop.builderClass.getEnclosingClass() == detailsClass) {
return fop;
}
}
throw new IllegalArgumentException("not found");
}
}
public interface PaymentDetails {
public interface Builder {
PaymentDetails build();
}
}
@JsonDeserialize(builder = PaymentMean.Builder.class)
public static class PaymentMean {
@JsonPOJOBuilder(withPrefix = "")
@JsonPropertyOrder({ "form_of_payment", "payment_details" })
public static class Builder {
private FormOfPayment formOfPayment;
private PaymentDetails paymentDetails;
public PaymentMean build () {
return new PaymentMean(this.formOfPayment, this.paymentDetails);
}
// if you annotate with @JsonIgnore, it works, but the value
// disappears in the constructor
public Builder formOfPayment (final FormOfPayment val) {
this.formOfPayment = val;
return this;
}
@JsonTypeInfo (use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "form_of_payment", visible = true)
@JsonTypeIdResolver (PaymentDetailsTypeIdResolver.class)
public Builder paymentDetails (final PaymentDetails val) {
this.paymentDetails = val;
return this;
}
}
public static Builder create() {
return new Builder();
}
protected final FormOfPayment formOfPayment;
protected final PaymentDetails paymentDetails;
PaymentMean (final FormOfPayment formOfPayment, final PaymentDetails paymentDetails) {
super ();
this.formOfPayment = formOfPayment;
this.paymentDetails = paymentDetails;
}
}
public static class PaymentDetailsTypeIdResolver extends TypeIdResolverBase {
@SuppressWarnings ("unchecked")
@Override
public String idFromValue (Object value) {
if (! (value instanceof PaymentDetails)) {
return null;
}
return FormOfPayment.fromDetailsClass ((Class<PaymentDetails>) value.getClass ()).name ();
}
@Override
public String idFromValueAndType (Object value, Class<?> suggestedType) {
return this.idFromValue (value);
}
@Override
public JavaType typeFromId(DatabindContext context, String id) {
return context.getTypeFactory().constructType(FormOfPayment.valueOf (id).getDetailsClass ());
}
@Override
public String getDescForKnownTypeIds() {
return "PaymentDetails";
}
@Override
public Id getMechanism() {
return JsonTypeInfo.Id.CUSTOM;
}
}
}
private final ObjectMapper MAPPER = jsonMapperBuilder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.build();
// [databind#1288]
@Test
public void testExternalWithCustomResolver() throws Exception
{
// given
final String asJson1 = a2q(
"{'form_of_payment':'INDIVIDUAL_CREDIT_CARD', 'payment_details':{'card_holder_first_name':'John',\n"
+"'card_holder_last_name':'Doe', 'number':'XXXXXXXXXXXXXXXX', 'expiry_date':'MM/YY',\n"
+ "'csc':666,'address':'10 boulevard de Sebastopol','zip_code':'75001','city':'Paris',\n"
+"'province':'Ile-de-France','country_code':'FR','description':'John Doe personal credit card'}}"
);
ClassesWithoutBuilder.PaymentMean ob1 = MAPPER.readValue(asJson1, ClassesWithoutBuilder.PaymentMean.class);
assertNotNull(ob1);
}
// [databind#1288]
@Test
public void testExternalWithCustomResolverAndBuilder() throws Exception
{
final String asJson2 = a2q(
"{'form_of_payment':'INSTRUMENTED_CREDIT_CARD',\n"
+"'payment_details':{\n"
+"'payment_instrument_id':'00000000-0000-0000-0000-000000000000',\n"
+" 'name':'Mr John Doe encrypted credit card'}}"
);
ClassesWithBuilder.PaymentMean ob2 = MAPPER.readValue(asJson2, ClassesWithBuilder.PaymentMean.class);
assertNotNull(ob2);
}
}