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;
}
}
Related
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.
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);
}
To be clear, let introduse some model:
interface A {
boolean isSomeCase();
}
class AAdapter implements JsonSerializer<A> {
public JsonElement serialize(A src, Type typeOfSrc, JsonSerializationContext context) {
if (src.isSomeCase()) {
/* some logic */
return result;
} else {
JsonObject json = new JsonObject();
JsonElement valueJson = <???>; // TODO serialize src like POJO
json.add(src.getClass().getSimpleName(), valueJson);
return json;
}
}
}
Gson gson = new GsonBuilder()
.registerTypeHierarchyAdapter(A.class. new AAdapter())
.create();
How it is possible to serealize some instance of A, which isSomeCase() = false, like any other object, that is serialized by ReflectiveTypeAdapterFactory.Adapter.
You can write a custom TypeAdapterFactory and handle incoming object's isSomeCase() result in its TypeAdapter's write() method and apply your logic there:
public class ATypeAdapterFactory implements TypeAdapterFactory {
public TypeAdapter<A> create(Gson gson, TypeToken type) {
if (!A.class.isAssignableFrom(type.getRawType())) {
// Check if incoming raw type is an instance of A interface
return null;
}
final TypeAdapter<A> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<A>() {
#Override
public void write(JsonWriter out, A value) throws IOException {
if(value.isSomeCase()) {
// your custom logic here
out.beginObject();
out.name("x").value(0);
out.endObject();
} else {
// default serialization here
delegate.write(out, value);
}
}
#Override
public A read(JsonReader in) throws IOException {
return delegate.read(in);
}
};
}
}
Test:
final GsonBuilder gsonBuilder = new GsonBuilder();
// Register custom type adapter factory
gsonBuilder.registerTypeAdapterFactory(new ATypeAdapterFactory());
final Gson gson = gsonBuilder.create();
A aSomeCaseTrue = new AImpl(true);
System.out.print("aSomeCaseTrue:" + gson.toJson(aSomeCaseTrue));
// writes; aSomeCaseTrue:{"x":0}
A aSomeCaseFalse = new AImpl(false);
System.out.print("aSomeCaseFalse:" + gson.toJson(aSomeCaseFalse););
// writes; aSomeCaseFalse:{"someCase":false}
Extras:
1) Your interface:
interface A {
boolean isSomeCase();
}
2) A sample class which implements your sample interface:
class AImpl implements A {
boolean someCase;
public AImpl(boolean value) {
this.someCase = value;
}
#Override
public boolean isSomeCase() {
return someCase;
}
}
I've this enum:
enum RequestStatus {
OK(200), NOT_FOUND(400);
private final int code;
RequestStatus(int code) {
this.code = code;
}
public int getCode() {
return this.code;
}
};
and in my Request-class, I have this field: private RequestStatus status.
When using Gson to convert the Java object to JSON the result is like:
"status": "OK"
How can I change my GsonBuilder or my Enum object to give me an output like:
"status": {
"value" : "OK",
"code" : 200
}
You can use something like this:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapterFactory(new MyEnumAdapterFactory());
or more simply (as Jesse Wilson indicated):
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(RequestStatus.class, new MyEnumTypeAdapter());
and
public class MyEnumAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
Class<? super T> rawType = type.getRawType();
if (rawType == RequestStatus.class) {
return new MyEnumTypeAdapter<T>();
}
return null;
}
public class MyEnumTypeAdapter<T> extends TypeAdapter<T> {
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
RequestStatus status = (RequestStatus) value;
// Here write what you want to the JsonWriter.
out.beginObject();
out.name("value");
out.value(status.name());
out.name("code");
out.value(status.getCode());
out.endObject();
}
public T read(JsonReader in) throws IOException {
// Properly deserialize the input (if you use deserialization)
return null;
}
}
}
In addition to Polet's answer, if you need a generic Enum serializer, you can achieve it via reflection:
public class EnumAdapterFactory implements TypeAdapterFactory
{
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type)
{
Class<? super T> rawType = type.getRawType();
if (rawType.isEnum())
{
return new EnumTypeAdapter<T>();
}
return null;
}
public class EnumTypeAdapter<T> extends TypeAdapter<T>
{
#Override
public void write(JsonWriter out, T value) throws IOException
{
if (value == null || !value.getClass().isEnum())
{
out.nullValue();
return;
}
try
{
out.beginObject();
out.name("value");
out.value(value.toString());
Arrays.stream(Introspector.getBeanInfo(value.getClass()).getPropertyDescriptors())
.filter(pd -> pd.getReadMethod() != null && !"class".equals(pd.getName()) && !"declaringClass".equals(pd.getName()))
.forEach(pd -> {
try
{
out.name(pd.getName());
out.value(String.valueOf(pd.getReadMethod().invoke(value)));
} catch (IllegalAccessException | InvocationTargetException | IOException e)
{
e.printStackTrace();
}
});
out.endObject();
} catch (IntrospectionException e)
{
e.printStackTrace();
}
}
public T read(JsonReader in) throws IOException
{
// Properly deserialize the input (if you use deserialization)
return null;
}
}
}
Usage:
#Test
public void testEnumGsonSerialization()
{
List<ReportTypes> testEnums = Arrays.asList(YourEnum.VALUE1, YourEnum.VALUE2);
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapterFactory(new EnumAdapterFactory());
Gson gson = builder.create();
System.out.println(gson.toJson(reportTypes));
}