did anyone tried to find a good solution to automatically convert all empty strings to null object on deserialization using JsonB (Yasson)?
I encountered this problem on migration from Jackson to Jsonb where empty string value in request produces deserialization exception since it cannot be parsed to object.
HTTP request payload:
{
fieldNameUid: '', // Java property is UUID type
}
Jackson had the following configuration:
public void customize(ObjectMapper mapper) {
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
SimpleModule module = new SimpleModule();
module.addDeserializer(
String.class,
new StdDeserializer<>(String.class) {
#Override
public String deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
String result = StringDeserializer.instance.deserialize(parser, context);
if (result == null || result.isBlank()) {
return null;
}
return result;
}
});
module.addDeserializer(byte[].class,
new StdDeserializer<>(byte[].class) {
#Override
public byte[] deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
String result = StringDeserializer.instance.deserialize(parser, context);
if (result == null || result.isBlank()) {
return null;
}
return result.getBytes(StandardCharsets.UTF_8);
}
});
mapper.registerModule(module);
}
and current Jsonb config:
public class JsonbObjectMapper implements JsonbConfigCustomizer {
#Override
public void customize(JsonbConfig jsonbConfig) {
jsonbConfig
.withDeserializers(new StringDeserializer(), new ByteArrayDeserializer(), new EnumDeserializer())
.withSerializers(new EnumSerializer());
}
public static class StringDeserializer implements JsonbDeserializer<String> {
#Override
public String deserialize(javax.json.stream.JsonParser jsonParser, javax.json.bind.serializer.DeserializationContext deserializationContext, Type type) {
final String str = jsonParser.getString();
return str == null || str.isBlank() ? null : str;
}
}
public static class ByteArrayDeserializer implements JsonbDeserializer<byte[]> {
#Override
public byte[] deserialize(javax.json.stream.JsonParser jsonParser, javax.json.bind.serializer.DeserializationContext deserializationContext, Type type) {
final String str = jsonParser.getString();
return str == null || str.isBlank() ? null : str.getBytes(StandardCharsets.UTF_8);
}
}
public static class EnumDeserializer implements JsonbDeserializer<Enum> {
#Override
public Enum deserialize(javax.json.stream.JsonParser jsonParser, javax.json.bind.serializer.DeserializationContext deserializationContext, Type type) {
final String str = jsonParser.getString();
if (str == null || str.isBlank()) {
return null;
}
for (final Enum en : ((Class<Enum>) type).getEnumConstants()) {
if (en.toString().equals(str)) {
return en;
}
}
return null;
}
}
public static class EnumSerializer implements JsonbSerializer<Enum> {
#Override
public void serialize(Enum anEnum, JsonGenerator jsonGenerator, SerializationContext serializationContext) {
jsonGenerator.write(anEnum == null ? null : anEnum.toString());
}
}
}
Since there is no alternative property for JsonB, I ended up writing custom deserialiser for UUID type.
Related
I have a JSON object with a Boolean property that needs to allow only true or false during the deserialization.
Any value different of true and false should throw an exception.
How can I do that?
e.g.:
Valid json:
{
"id":1,
"isValid":true
}
Invalid json:
{
"id":1,
"isValid":1
}
Update
I tried what #MichaĆ Ziober proposed and it worked fine.
As I'm implementing a Spring application (with Webflux) I just had to configure in a different way. I'm posting here what I did:
Create a Configuration class extending from DelegatingWebFluxConfiguration
Override the configureHttpMessageCodecs method setting the property on ObjectMapper
#Configuration
public class JacksonConfig extends DelegatingWebFluxConfiguration {
#Override
protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
configurer.defaultCodecs()
.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper));
}
}
You need to disable ALLOW_COERCION_OF_SCALARS feature:
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = new ObjectMapper();
mapper.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS);
System.out.println(mapper.readValue(jsonFile, Pojo.class));
}
}
class Pojo {
private int id;
private Boolean isValid;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Boolean getIsValid() {
return isValid;
}
public void setIsValid(Boolean valid) {
isValid = valid;
}
#Override
public String toString() {
return "Pojo{" +
"id=" + id +
", isValid=" + isValid +
'}';
}
}
Above code prints:
Exception in thread "main"
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot
coerce Number (1) for type java.lang.Boolean (enable
MapperFeature.ALLOW_COERCION_OF_SCALARS to allow) at [Source:
(File); line: 3, column: 14] (through reference chain:
Pojo["isValid"]) at
com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
Well, it's not a for-any-case implementation, but may be it'll be suitable for you:
public class TestBooleanDeserializer extends JsonDeserializer<Boolean> {
#Override
public Boolean deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
String str = jsonParser.getText();
boolean val = Boolean.valueOf(str);
if (!val && Objects.nonNull(str) && !Objects.equals("false", str.toLowerCase())) {
throw new RuntimeException("invalid JSON");
}
return val;
}
}
Since a Boolean.valueOf is a result of check of the java.lang.Boolean#
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
here: boolean val = Boolean.valueOf(str); you'll check is it a true value or not. If not, you should check is it null or false: if (!val && Objects.nonNull(str) && !Objects.equals("false", str.toLowerCase()))
if it's not you can throw an exception whatever you need. Otherwise you'll return a boolean value
I have wrote custom deserializer for my type, which is represented as interface Attachment and there are two implementions of this interface Photo and Video.
When parsing I recognize them from json using discriminator field.
Now I'm facing problem when jp.getCodec() returns null, leading
to null pointer exception
Why this is happining and how to fix it?
public class AttachmentDeserializer extends StdDeserializer<Attachment> {
ObjectMapper objectMapper = new ObjectMapper();
public AttachmentDeserializer() {
this(null);
objectMapper.registerModule(new Jdk8Module());
}
public AttachmentDeserializer(Class<Attachment> t) {
super(t);
objectMapper.registerModule(new Jdk8Module());
}
#Override
public Attachment deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String type = node.get("type").asText();
switch (type) {
case "photo":
return new AttachmentPhoto(
node.get("t").asInt(),
objectMapper.readValue(node.get("photo").traverse(), Photo.class));
case "video":
return new AttachmentVideo(
node.get("t").asInt(),
objectMapper.readValue(node.get("video").traverse(), Video.class));
default:
throw ctxt.weirdStringException("type", Attachment.class, "Unknown discriminator");
}
}
}
The attachmentPhoto code:
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class AttachmentPhoto implements Attachment {
private Photo photo;
public Attachments what() {
return Attachments.ATTACHMENT_PHOTO;
}
public String getDiscriminator() {
return "photo";
}
public AttachmentPhoto() {}
public AttachmentPhoto(Photo photo) {
this.photo = photo;
}
public Photo getPhoto() {
return this.photo;
}
public AttachmentPhoto setPhoto(Photo v) {
this.photo = v;
return this;
}
public boolean isAttachmentPhoto() {
return true;
}
public AttachmentPhoto asAttachmentPhoto() {
return this;
}
public boolean isAttachmentVideo() {
return false;
}
public AttachmentVideo asAttachmentVideo() {
throw new IllegalStateException("Not a $stName: " + this);
}
#Override
public boolean equals(Object thatObj) {
if (this == thatObj) return true;
if (!(thatObj instanceof AttachmentPhoto)) return false;
AttachmentPhoto that = (AttachmentPhoto) thatObj;
return this.photo.equals(that.photo);
}
#Override
public String toString() {
return "AttachmentPhoto{" + "photo=" + this.photo + '}';
}
}
Your default constructor looks very suspicious for two reasons, first it calls the second constructor with null class type which then passes the null type to the superclass hence the overridden generic method is messed up when this constructor is used. Secondly, it does no useful work since it already calls the other constructor that initializes objectMapper. You should remove the first constructor and remain with just the typed one and initialize your deserializer using that.
I have a JSON string and I want to alter the value while constructing the JsonNode using Jackson library.
eg:-
input: {"name":"xyz","price":"90.00"}
output:{"name":"xyz-3","price":90.90}
I created my own JsonFactory and passed my own Parser. but I can only alter the keys, not the values associated with a key.
code:
private static ObjectMapper create() {
ObjectMapper objectMapper = new ObjectMapper(new JsonFactory() {
#Override
protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(data, offset, len, ctxt));
}
#Override
protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(in, ctxt));
}
#Override
protected JsonParser _createParser(Reader r, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(r, ctxt));
}
#Override
protected JsonParser _createParser(char[] data, int offset, int len, IOContext ctxt, boolean recyclable)
throws IOException {
return new MyParser(super._createParser(data, offset, len, ctxt, recyclable));
}
});
private static final class MyParser extends JsonParserDelegate {
private MyParser(JsonParser d) {
super(d);
}
#Override
public String getCurrentName() throws IOException, JsonParseException {
....
}
#Override
public String getText() throws IOException, JsonParseException {
...
}
#Override
public Object getCurrentValue() {
...
}
#Override
public String getValueAsString() throws IOException {
...
}
#Override
public String getValueAsString(String defaultValue) throws IOException {
...
}
}
Below is the code to construct the JsonNode from the string.
mapper.readTree(jsonStr);
In this case when the readTree method is called the getCurrentValue or getValueAsString methods are not called, so I am not able to alter the value while creating the JsonNode itself.
Also the json strings can be different. Basically I want to construct a JsonNode from the string. so tying to a specific schema/bean is not a good choice here.
How to address this ? TIA
Adding the updated code for version 2.7.4:-
static class MyParser extends JsonParserDelegate {
MyParser(final JsonParser delegate) {
super(delegate);
}
#Override
public String getText() throws IOException {
final String text = super.getText();
if ("name".equals(getCurrentName())) {
return text + "-3";
}
return text;
}
#Override
public JsonToken nextToken() throws IOException {
if ("price".equals(getCurrentName())) {
// Advance token anyway
super.nextToken();
return JsonToken.VALUE_NUMBER_FLOAT;
}
return super.nextToken();
}
#Override
public int getCurrentTokenId() {
try {
if ("price".equals(getCurrentName())) {
return JsonTokenId.ID_NUMBER_FLOAT;
}
} catch (final IOException e) {
//
}
return super.getCurrentTokenId();
}
#Override
public NumberType getNumberType() throws IOException {
if ("price".equals(getCurrentName())) {
return NumberType.FLOAT;
}
return super.getNumberType();
}
#Override
public float getFloatValue() throws IOException {
return Float.parseFloat(getValueAsString("0")) + 0.09F;
}
#Override
public double getDoubleValue() throws IOException {
return Double.parseDouble(getValueAsString("0")) + 0.09D;
}
}
pom.xml:-
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.8.7</version>
<!--<scope>test</scope>-->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.8.7</version>
</dependency>
Edit: there is a subtle difference between 2.7.* and 2.9.*.
While 2.9.* is able to differentiate between double and float with
getDoubleValue()
getFloatValue()
instead 2.7.* only uses
getDoubleValue()
even for ID_NUMBER_FLOAT tokens.
So, you need to decide if you want to maintain retro-compatibility or not.
You can also override both, like I did here.
This is all what you need for your custom MyParser
static class MyParser extends JsonParserDelegate {
MyParser(final JsonParser delegate) {
super(delegate);
}
#Override
public String getText() throws IOException {
final String text = super.getText();
if ("name".equals(getCurrentName())) {
return text + "-3";
}
return text;
}
#Override
public JsonToken nextToken() throws IOException {
if ("price".equals(getCurrentName())) {
// Advance token anyway
super.nextToken();
return JsonToken.VALUE_NUMBER_FLOAT;
}
return super.nextToken();
}
#Override
public int getCurrentTokenId() {
try {
if ("price".equals(getCurrentName())) {
return JsonTokenId.ID_NUMBER_FLOAT;
}
} catch (final IOException e) {
//
}
return super.getCurrentTokenId();
}
#Override
public NumberType getNumberType() throws IOException {
if ("price".equals(getCurrentName())) {
return NumberType.FLOAT;
}
return super.getNumberType();
}
#Override
public float getFloatValue() throws IOException {
return Float.parseFloat(getValueAsString("0")) + 0.09F;
}
#Override
public double getDoubleValue() throws IOException {
return Double.parseDouble(getValueAsString("0")) + 0.09D;
}
}
Output: {"name":"xyz-3","price":90.09}
Your code seems fine, and it's tested and working ;)
Are you really sure that regarding the Separation of Concerns it is a good idea to mix parsing and changes within the parsed data?
If you still want to do this, you could use a Custom Deserializer and treat your wanted field names and types the way you want it, like:
class CustomDeserializer extends StdDeserializer<Entity> {
public CustomDeserializer(Class<Entity> t) {
super(t);
}
#Override
public Entity deserialize(JsonParser jp, DeserializationContext dc) throws IOException {
String name = null;
float price = 0;
JsonToken currentToken = null;
while ((currentToken = jp.nextValue()) != null) {
switch (currentToken) {
case VALUE_STRING:
switch (jp.getCurrentName()) {
case "name":
name = jp.getText() + "-3"; // change this text to whatever you want;
break;
case "price":
price = Float.parseFloat(jp.getText()); // parse
break;
default:
break;
}
break;
default:
break;
}
}
return new Entity(name, price);
}
}
And after registering your custom deserializer it works on any object mapper you want:
#Test
public void customDeserialization() throws IOException {
// given
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Entity.class, new CustomDeserializer(Entity.class));
mapper.registerModule(module);
// when
Entity entity = mapper.readValue("{\"name\":\"xyz\",\"price\":\"90.00\"}", Entity.class);
// then
assertThat(entity.getName()).isEqualTo("xyz-3");
assertThat(entity.getPrice()).isEqualTo(90f);
}
I'm using Gson with retrofit. My server accepts null values so in the gson builder I have given
gsonBuilder.serializeNulls();
So that null values will not be ignored. But in some APIs I have a special case, where some of the fields should be present even is its's null, and some other fields should not serialize if it is null.
for example If I have a request class like
class Request {
String id;
String type;
}
and if I have a request where id=null and type=null , gson should serialize it like:
{
id : null
}
Which means, if type is null it should ignore that field, but if id is null, it should present as null in the request.
Currently it is serializing like:
{
id : null,
type:null
}
since I have given gsonBuilder.serializeNulls(); . How can I deal with this special case?
Here is my code:
public static void main(String[] args) throws SecurityException,
NoSuchFieldException, ClassNotFoundException {
GsonBuilder gson = new GsonBuilder();
Request request = new Request();
Gson g = gson.serializeNulls()
.setExclusionStrategies(new TestExclStrat(request)).create();
System.out.println(g.toJson(request));
}
class Request {
public String id;
public String type;
}
class TestExclStrat implements ExclusionStrategy {
private Object obj;
public TestExclStrat(Object obj) throws SecurityException,
NoSuchFieldException, ClassNotFoundException {
this.obj = obj;
}
public boolean shouldSkipClass(Class<?> arg0) {
return false;
}
public boolean shouldSkipField(FieldAttributes f) {
if (obj instanceof Request) {
System.out.println(f.getName());
if (!"id".equals(f.getName())) {
try {
Field field = obj.getClass().getDeclaredField(f.getName());
field.setAccessible(true);
if (field.get(obj) == null) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
return false;
}
}
I am going to deserialize Json null value to Java Object empty string
I am able to make my custom deserializer, but when the Json value is null, it did not go into the deserializer.
How should I deserialize it?
Thanks in advance!
public class CustomStringDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jsonparser, DeserializationContext deserializationcontext) throws IOException,
JsonProcessingException {
String str = jsonparser.getText();
try {
return (str == null) ? "" : str;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public CustomObjectMapper() {
SimpleModule _module = new SimpleModule("Module", new Version(1, 9, 10, "FINAL"));
_module.addDeserializer(String.class, new CustomStringDeserializer());
}
Thanks #nutlike
I do this by
#Override
public String getNullValue() {
return "";
}
Maybe it would be sufficient to overwrite the method getNullValue()?
public class CustomStringDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jsonparser,
DeserializationContext deserializationcontext) throws IOException,
JsonProcessingException {
return jsonparser.getText();
}
#Override
public String getNullValue() {
return "";
}
}