Serializing Java boolean array as 0 & 1s without a custom Serializer - java

I have a ConcurrentHashMap<Long, boolean[]> that I want to send to a client app. To optimize the size, I'd like to use 0 and 1 for false and true, respectively.
I have an ObjectMapper that I instantiate so I have tried this:
objectMapper.configOverride( boolean.class ).setFormat( JsonFormat.Value.forShape( Shape.NUMBER_INT ) ) ;
That did not make it produce 0 and 1s.
Can this be done using only configuration or will I have to create a custom Serializer?
Thanks

Using this class to test:
public class Sample {
public Sample(String name, boolean flag) {
this.name = name;
this.flag = flag;
}
private String name;
private boolean flag;
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And trying the OP's approach to convert a Boolean value into a number
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configOverride(boolean.class).setFormat(JsonFormat.Value.forShape(Shape.NUMBER_INT));
Sample sample1 = new Sample("foo", true);
objectMapper.writeValue(new File("sample1.json"), sample1);
Resulted in this:
{"name":"foo","flag":true}
So, the conversion didn't work. The OP indicated the typical 1 and 0 values for true and false respectively. The mistake? .configOverride(Boolean.class) instead of .configOverride(boolean.class) should be used. After that change, sample1.json
is now
{"name":"foo","flag":1}
Although that works, we run into a problem with deserialization. You can't just convert a 1 or 0 into a Boolean. For this, the best approach is to create serializer and deserializer classes that can be used to convert to/from Boolean. Then, these classes must be added as modules to the object mapper.
Deserializer
public class NumericBooleanDeserializer extends JsonDeserializer<Boolean>{
#Override
public Boolean deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
if ("1".equals(p.getText())) {
return Boolean.TRUE;
}
if ("0".equals(p.getText())) {
return Boolean.FALSE;
}
return null;
}
}
Serializer
public class NumericBooleanSerializer extends JsonSerializer<Boolean>{
#Override
public void serialize(Boolean value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeString(value ? "1" : "0");
}
}
Now, we need to make two changes to Sample class. One, add a no-argument contructor, and annotate the boolean field.
public static class Sample {
public Sample() {}
public Sample(String name, boolean flag) {
this.name = name;
this.flag = flag;
}
private String name;
#JsonSerialize(using = NumericBooleanSerializer.class)
#JsonDeserialize(using = NumericBooleanDeserializer.class)
private boolean flag;
// rest of the class omitted
}
Now, if I update my test main() method as follows
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configOverride(Boolean.class).setFormat(JsonFormat.Value.forShape(Shape.NUMBER_INT));
SimpleModule module = new SimpleModule();
module.addSerializer(Boolean.class, new NumericBooleanSerializer());
module.addDeserializer(Boolean.class, new NumericBooleanDeserializer());
objectMapper.registerModule(module);
Sample sample1 = new Sample("foo", true);
System.out.println("Sample1: " + sample1);
objectMapper.writeValue(new File("sample1.json"), sample1);
Sample sample2 = objectMapper.readValue(Paths.get("sample1.json").toFile(), Sample.class);
System.out.println("Sample2: " + sample2);
The program outputs:
Sample1: { name :foo, flag : true }
Sample2: { name :foo, flag : true }
while the created file sample1.json contains the converted values for Boolean
{"name":"foo","flag":"1"}
UPDATE: For this simple example, the serializer is not really needed. The only difference between using the serializer and when you use the config override, is that the numeric value for the boolean is wrapped in double quotes when the serializer is used. The deserializer does not care about this small detail and it's able to convert the number to Boolean just the same. I just thought it was a good idea to show how to create and use the serializer.

Related

Java Json Serialization for Spring Project

I have a pojo class, for example :
Class A Pojo:
public class A{
private String field1;
private String field2;
#JsonSerialize(using = NumberFormatterToString.class, as = String.class)
private Integer field3;
//getters and setters
}
Now while returning field3 from spring REST API, i want it convert to something like
Input :
field3 - 312548
Output
field3 - "312,548"
I have written custom class JsonSerializer to do so:
Custom JsonSerializer:
public class NumberFormatterToString extends JsonSerializer<Integer> {
#Override
public void serialize(Integer value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
jsonGenerator.writeObject(convertIntegerNumberFormat(value));
}
public static String convertIntegerNumberFormat(Integer i) {
NumberFormat myFormat = NumberFormat.getInstance();
myFormat.setGroupingUsed(true);
return i != null ? myFormat.format(i) : null;
}
public static String convertDecimalNumberFormat(Double i) {
DecimalFormat decimalFormat = new DecimalFormat("#.0000");
decimalFormat.setGroupingUsed(true);
decimalFormat.setGroupingSize(3);
return i != null ? decimalFormat.format(i) : null;
}
}
If i use this Annotation it converts it even while internal operations and thus causes already written Integer based logic to fail.
Thus i want to configure it in a way that, for all internal operation it should consider Integer, only while returning the response via API it should convert it to the String value.
I am not sure how exactly should i configure this?
Probably all you have to do is to create custom deserializer. Try to modify your pojo in a similar way:
public class A {
private String field1;
private String field2;
#JsonDeserializer(using = NumberFormatterToInteger.class)
#JsonSerialize(using = NumberFormatterToString.class, as = String.class)
private Integer field3;
}
and create custom class that extend JsonDeserializer
public class NumberFormatterToInteger extends JsonDeserializer<Integer> {
#Override
public Integer deserialize(JsonParser parser, DeserializationContext context) {
return YourParser.toInt(parser.getText()); // some logic that could look like that
}
}
Hope it will work.
Assume the definition of DTO is below:
#JsonSerialize(using = InfoSerializer.class)
#JsonDeserialize(using = InfoDeserializer.class)
class Info {
private String name;
private String address;
private Integer age;
}
Info's Serializer and Deserializer definition
class InfoSerializer extends JsonSerializer<Info> {
#Override
public void serialize(Info value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("name", value.getName());
jsonGenerator.writeStringField("address", value.getAddress());
jsonGenerator.writeStringField("age", value.getAge().toString());
jsonGenerator.writeEndObject();
}
}
class InfoDeserializer extends JsonDeserializer<Info> {
#Override
public Info deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
int age = Integer.parseInt(node.get("age").asText());
String name = node.get("name").asText();
String address = node.get("address").asText();
Info info = new Info();
info.setName(name);
info.setAddress(address);
info.setAge(age);
return info;
}
}
Test controller
#PostMapping(value = "/test/mapper")
public Mono<Info> test(#RequestBody Info info) {
System.out.println(info);
return Mono.just(info);
}
input
{
"name":"huawei",
"address":"shen zhen",
"age":"31"
}
Test controller print message
Info{name='huawei', address='shen zhen', age=31}
The response client get
{
"name": "huawei",
"address": "shen zhen",
"age": "31"
}
I have finally called this function anywhere where change was expected in the logic of code.
public static String convertIntegerNumberFormat(Integer i) {
NumberFormat myFormat = NumberFormat.getInstance();
myFormat.setGroupingUsed(true);
return i != null ? myFormat.format(i) : null;
}

Jackson Custom Deserializer for polymorphic objects and String literals as defaults

I'd like to deserialize an object from YAML with the following properties, using Jackson in a Spring Boot application:
Abstract class Vehicle, implemented by Boat and Car
For simplicity, imagine both have a name, but only Boat has also a seaworthy property, while Car has a top-speed.
mode-of-transport:
type: boat
name: 'SS Boatface'
seaworthy: true
----
mode-of-transport:
type: car`
name: 'KITT'
top-speed: 123
This all works fine in my annotated subclasses using #JsonTypeInfo and #JsonSubTypes!
Now, I'd like to create a shorthand using only a String value, which should create a Car by default with that name:
mode-of-transport: 'KITT'
I tried creating my own custom serializer, but got stuck on most of the relevant details. Please help me fill this in, if this is the right approach:
public class VehicleDeserializer extends StdDeserializer<Merger> {
/* Constructors here */
#Override
public Vehicle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (/* it is an OBJECT */){
// Use the default polymorphic deserializer
} else if (/* it is a STRING */) {
Car car = new Car();
car.setName( /* the String value */ );
return car;
}
return ???; /* what to return here? */
}
}
I found these 2 answers for inspiration, but it looks like combining it with polymorphic types makes it more difficult: How do I call the default deserializer from a custom deserializer in Jackson and Deserialize to String or Object using Jackson
A few things are different than the solutions offered in those questions:
I am processing YAML, not JSON. Not sure about the subtle differences there.
I have no problem hardcoding the 'default' type for Strings inside my Deserializer, hopefully making it simpler.
This was actually easier than I thought to solve it. I got it working using the following:
Custom deserializer implementation:
public class VehicleDeserializer extends StdDeserializer<Vehicle> {
public VehicleDeserializer() {
super(Vehicle.class);
}
#Override
public Vehicle deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.currentToken() == JsonToken.VALUE_STRING) {
Car car = new Car();
car.setName(jp.readValueAs(String.class));
return car;
}
return jp.readValueAs(Vehicle.class);
}
}
To avoid circular dependencies and to make the custom deserializer work with the polymorphic #JsonTypeInfo and #JsonSubTypes annotations I kept those annotations on the class level of Vehicle, but put the following annotations on the container object I am deserializing:
public class Transport {
#JsonDeserialize(using = VehicleDeserializer.class)
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
private Vehicle modeOfTransport;
// Getter, setters
}
This means that by default a Vehicle is deserialized as a polymorphic object, unless explicitly specified to deserialize it using my custom deserializer. This deserializer will then in turn defer to the polymorphism if the input is not a String.
Hopefully this will help someone running into this issue :)
So there is a solution that requires you to handle the jackson errors using a DeserializationProblemHandler (since you want to parse the same type using different inputs, this is not achieved easily using regular means):
public class MyTest {
#Test
public void doTest() throws JsonParseException, JsonMappingException, IOException {
final ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(final DeserializationContext ctxt, final Class<?> instClass, final JsonParser p, final String msg) throws IOException {
if (instClass.equals(Car.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return new Car();
}
}
return NOT_HANDLED;
}
#Override
public JavaType handleMissingTypeId(final DeserializationContext ctxt, final JavaType baseType, final TypeIdResolver idResolver, final String failureMsg) throws IOException {
// if (baseType.isTypeOrSubTypeOf(Vehicle.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return TypeFactory.defaultInstance().constructType(Car.class);
}
return super.handleMissingTypeId(ctxt, baseType, idResolver, failureMsg);
}
});
final Container objectValue = om.readValue(getObjectJson(), Container.class);
assertTrue(objectValue.getModeOfTransport() instanceof Car);
final Container stringValue = om.readValue(getStringJson(), Container.class);
assertTrue(stringValue.getModeOfTransport() instanceof Car);
}
private String getObjectJson() {
return "{ \"modeOfTransport\": { \"type\": \"car\", \"name\": \"KITT\", \"speed\": 1}}";
}
private String getStringJson() {
return "{ \"modeOfTransport\": \"KITT\"}";
}
}
class Container {
private Vehicle modeOfTransport;
public Vehicle getModeOfTransport() {
return modeOfTransport;
}
public void setModeOfTransport(final Vehicle modeOfTransport) {
this.modeOfTransport = modeOfTransport;
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
#JsonSubTypes({
#Type(name = "car", value = Car.class)
})
abstract class Vehicle {
protected String type;
protected String name;
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
#JsonTypeName("car")
class Car extends Vehicle {
private int speed;
public int getSpeed() {
return speed;
}
public void setSpeed(final int speed) {
this.speed = speed;
}
}
Note that I used JSON, not YAML, and you need to add your other subtypes as well.

Json Deserialization in Java /w Jackson of mixed types, contained in one array

Consider the following json, getting from an public API:
anyObject : {
attributes: [
{
"name":"anyName",
"value":"anyValue"
},
{
"name":"anyName",
"value":
{
"key":"anyKey",
"label":"anyLabel"
}
}
]
}
As you can see, sometimes the value is a simple string and sometimes its an object. Is it somehow possible to deserialize those kind of json-results, to something like:
class AnyObject {
List<Attribute> attributes;
}
class Attribute {
private String key;
private String label;
}
How would I design my model to cover both cases. Is that possible ?
Despite being hard to manage as others have pointed out, you can do what you want. Add a custom deserializer to handle this situation. I rewrote your beans because I felt your Attribute class was a bit misleading. The AttributeEntry class in the object that is an entry in that "attributes" list. The ValueObject is the class that represents that "key"/"label" object. Those beans are below, but here's the custom deserializer. The idea is to check the type in the JSON, and instantiate the appropriate AttributeEntry based on its "value" type.
public class AttributeDeserializer extends JsonDeserializer<AttributeEntry> {
#Override
public AttributeEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
String name = root.get("name").asText();
if (root.get("value").isObject()) {
// use your object mapper here, this is just an example
ValueObject attribute = new ObjectMapper().readValue(root.get("value").asText(), ValueObject.class);
return new AttributeEntry(name, attribute);
} else if (root.get("value").isTextual()) {
String stringValue = root.get("value").asText();
return new AttributeEntry(name, stringValue);
} else {
return null; // or whatever
}
}
}
Because of this ambiguous type inconvenience, you will have to do some type checking throughout your code base.
You can then add this custom deserializer to your object mapper like so:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(AttributeEntry.class, new AttributeDeserializer());
objectMapper.registerModule(simpleModule);
Here's the AttributeEntry:
public class AttributeEntry {
private String name;
private Object value;
public AttributeEntry(String name, String value) {
this.name = name;
this.value = value;
}
public AttributeEntry(String name, ValueObject attributes) {
this.name = name;
this.value = attributes;
}
/* getters/setters */
}
Here's the ValueObject:
public class ValueObject {
private String key;
private String label;
/* getters/setters */
}

Setting Default value to a variable when deserializing using gson

I am trying to convert JSON to Java object. When a certain value of a pair is null, it should be set with some default value.
Here is my POJO:
public class Student {
String rollNo;
String name;
String contact;
String school;
public String getRollNo() {
return rollNo;
}
public void setRollNo(String rollNo) {
this.rollNo = rollNo;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
}
Example JSON object:
{
"rollNo":"123", "name":"Tony", "school":null
}
So if school is null, I should make this into a default value, such as "school":"XXX". How can I configure this with Gson while deserializing the objects?
If the null is in the JSON, Gson is going to override any defaults you might set in the POJO. You could go to the trouble of creating a custom deserializer, but that might be overkill in this case.
I think the easiest (and, arguably best given your use case) thing to do is the equivalent of Lazy Loading. For example:
private static final String DEFAULT_SCHOOL = "ABC Elementary";
public String getSchool() {
if (school == null) school == DEFAULT_SCHOOL;
return school;
}
public void setSchool(String school) {
if (school == null) this.school = DEFAULT_SCHOOL;
else this.school = school;
}
Note: The big problem with this solution is that in order to change the Defaults, you have to change the code. If you want the default value to be customizable, you should go with the custom deserializer as linked above.
I think that the way to do this is to either write your no-args constructor to fill in default values, or use a custom instance creator. The deserializer should then replace the default values for all attributes in the JSON object being deserialized.
I was having the same issue, until I found this great solution.
For reference, you can create a post-processing class:
interface PostProcessable {
fun gsonPostProcess()
}
class PostProcessingEnabler : TypeAdapterFactory {
fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
val delegate = gson.getDelegateAdapter(this, type)
return object : TypeAdapter<T>() {
#Throws(IOException::class)
fun write(out: JsonWriter, value: T) {
delegate.write(out, value)
}
#Throws(IOException::class)
fun read(`in`: JsonReader): T {
val obj = delegate.read(`in`)
if (obj is PostProcessable) {
(obj as PostProcessable).gsonPostProcess()
}
return obj
}
}
}
}
Register it like this:
GsonBuilder().registerTypeAdapterFactory(PostProcessingEnabler())
Implement it on your model:
class MyClass : Serializable, PostProcessable {
// All your variable data
override fun gsonPostProcess() {
// All your post processing logic you like on your object
// set default value for example
}
}
And finally use it when converting json string:
var myObject = myGson.fromJson(myObjectJson, MyClass::class)
Or using retrofit2:
val api = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(
GsonConverterFactory.create(
GsonBuilder().registerTypeAdapterFactory(
GsonPostProcessingEnabler()
).create()
)
)
.client(OkHttpClient.Builder().build())
.build()
.create(AccountApi::class.java)
You can simply make a universal function that checks for null
model.SchoolName= stringNullChecker(model.SchoolName);
public static String stringNullChecker(String val) {
if (null == val) val = "";
return val;
}

Exclude classes and namespaces from serialization?

I'm trying to serialize a rather large structure with Jackson.
However, it's also trying to export a lot of substructures I will never need (causing a JsonMappingException: No serializer found for class)
So how can I exclude classes and namespaces from serialization?
Alternatively, how can I flag properties of my classes as excluded/ignored?
Use the transient keyword if you have actually access to the substructure you want to exclude.
transient is a Java keyword which marks a member variable not to be
serialized when it is persisted to streams of bytes. When an object is
transferred through the network, the object needs to be 'serialized'.
Serialization converts the object state to serial bytes. Those bytes
are sent over the network and the object is recreated from those
bytes. Member variables marked by the java transient keyword are not
transferred, they are lost intentionally.
http://en.wikibooks.org/wiki/Java_Programming/Keywords/transient
Please give an example for exclude classes and namespace but for properties for which you might not control the source code you can use the following on types and fields
#JsonIgnoreProperties(value = {"propertyName", "otherProperty"})
Here's the javadoc.
Here's an example
#JsonIgnoreProperties(value = { "name" })
public class Examples {
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
Examples examples = new Examples();
examples.setName("sotirios");
Custom custom = new Custom();
custom.setValue("random");
custom.setNumber(42);
examples.setCustom(custom);
ObjectMapper mapper = new ObjectMapper();
StringWriter writer = new StringWriter();
mapper.writeValue(writer, examples);
System.out.println(writer.toString());
}
private String name;
#JsonIgnoreProperties(value = { "value" })
private Custom custom;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Custom getCustom() {
return custom;
}
public void setCustom(Custom custom) {
this.custom = custom;
}
static class Custom {
private String value;
private int number;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
}
prints
{"custom":{"number":42}}
In other words, it ignored Examples#name and Custom#value.

Categories

Resources