Search code examples
springspring-bootjackson-databind

How to properly implement a Spring Converter?


I have a Money class with factory methods for numeric and String values. I would like to use it as a property of my input Pojos.

I created some Converters for it, this is the String one:

@Component
public class StringMoneyConverter implements Converter<String, Money> {
    @Override
    public Money convert(String source) {
        return Money.from(source);
    }
}

My testing Pojo is very simple:

public class MoneyTestPojo {
   private Money value;
   //getter and setter ommited
}

I have an endpoint which expects a Pojo:

@PostMapping("/pojo")
public String savePojo(@RequestBody MoneyTestPojo pojo) {
//...
}

Finally, this is the request body:

{
   value: "100" 
}

I have the following error when I try this request:

JSON parse error: Cannot construct instance of br.marcellorvalle.Money (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('100'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of br.marcellorvalle.Money (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('100')\n at [Source: (PushbackInputStream); line: 8, column: 19] (through reference chain: br.marcellorvalle.MoneytestPojo[\"value\"])",

If I change Money and add a constructor which receives a String this request works but I really need a factory method as I have to deliver special instances of Money on specific cases (zeros, nulls and empty strings).

Am I missing something?

Edit: As asked, here goes the Money class:

public class Money {
    public static final Money ZERO = new Money(BigDecimal.ZERO);

    private static final int PRECISION = 2;
    private static final int EXTENDED_PRECISION = 16;
    private static final RoundingMode ROUNDING = RoundingMode.HALF_EVEN;

    private final BigDecimal amount;

    private Money(BigDecimal amount) {
        this.amount = amount;
    }

    public static Money from(float value) {
        return Money.from(BigDecimal.valueOf(value));
    }

    public static Money from(double value) {
        return Money.from(BigDecimal.valueOf(value));
    }

    public static Money from(String value) {
        if (Objects.isNull(value) || "".equals(value)) {
            return null;
        }

        return Money.from(new BigDecimal(value));
    }

    public static Money from(BigDecimal value) {
        if (Objects.requireNonNull(value).equals(BigDecimal.ZERO)) {
            return Money.ZERO;
        }

        return new Money(value);
    }
//(...)
}

Solution

  • It seems that using the org.springframework.core.convert.converter.Converter only works if the Money class is a "@PathVariable" in the controller.

    I finally solved it using the com.fasterxml.jackson.databind.util.StdConverter class:

    I created the following Converter classes:

    public class MoneyJsonConverters {
        public static class FromString extends StdConverter<String, Money> {
            @Override
            public Money convert(String value) {
                return Money.from(value);
            }
        }
    
        public static class ToString extends StdConverter<Money, String> {
            @Override
            public String convert(Money value) {
                return value.toString();
            }
        }
    }
    

    Then I annotated the Pojo with @JsonDeserialize @JsonSerialize accordingly:

    public class MoneyTestPojo {
       @JsonSerialize(converter = MoneyJsonConverters.ToString.class)
       @JsonDeserialize(converter = MoneyJsonConverters.FromString.class)
       private Money value;
       //getter and setter ommited
    }