I am constructing my POJO directly from the received JSON using Jackson's ObjectMapper. For that purpose I annotate my fields (in this case fields are abstract methods because I'm using AutoValue to reduce boilerplate) with #JsonProperty annotations.
Now, some of the fields in my POJO are enums, where JSON obviously contains a regular string. I would like to somehow validate that those JSON fields do indeed submit to the given enum type before the object is constructed.
Here is the example.
#JsonProperty(value = "messageType")
public abstract Optional<MessageType> messageType();
This property is of type enum MessageType, and the corresponding JSON field is ie. "messageType": "A_1" which is a string.
Now, if the "messageType" is something like "blabla" I'd like the validation to fail.
Is there any way to validate that with annotations using Hibernate Validator?
#JsonProperty(value = "messageType")
public abstract Optional messageType();
Assumed you have, MessageType as enum with distinct values A_1, A_2...
Create a static utility method to serialize/deserialize for the enum values from the received/sent json values
public static <T extends Enum<T>> T getEnumFromJson(Class<T> enumClass, String value)
{
if(enumClass == null) throw new IllegalArgumentException("EnumClass value can't be null");
for(Enum<?> enumValue : enumClass.getEnumConstants()){
if(enumValue.toString().equalsIgnoreCase(value)){
return (T) enumValue;
}
}
//Validation message construct to give more meaningful details to end-user
StringBuilder erroMsg = new StringBuilder();
boolean bFirst = true;
for(Enum<?> enumValue : enumClass.getEnumConstants()) {
errorMessage.append(bFirst ? "": ", ").append(enumValue);
bFirst = false;
}
throw new IllegalArgumentException(value + " is invalid value, Supported value are "+ errorMessage);
}
//Enum as represented below
public enum MessageType {
A_1,
A_2,
A_3
#JsonCreator
public static MessageType fromValue(String value){
return getEnumFromJson(MessageType.class, value);
}
#JsonValue
public String toJson(){
return name().toLowerCase();
}
}
Related
I have 2 implementations of Value interface, RangeValue, FileValue.
RangeValue looks like below:
public class RangeValue implements Value {
private int min;
private int max;
public RangeValue(int min, int max) {
this.min = min;
this.max = max;
}
public int getMin() {
return min;
}
public int getMax() {
return max;
}
}
FileValue looks like below:
public class FileValue implements Value {
private String contentType;
private String value;
public FileValue(String contentType, String value) {
this.contentType = contentType;
this.value = value;
}
public String getContentType() {
return contentType;
}
public String getValue() {
return value;
}
}
the json for RangeValue looks like :
{
"min": 200,
"max": 300
}
The json for FileValue looks:
{
"contentType": "application/octet-stream",
"value": "fileValue"
}
Now I want the RequestType parameter for these json to be of type Value only, I can't change the JSON files i.e. the json would look like the same and user should use the same JSON in request body as stated above.
I solved this by using #JsonTypeInfo & #JsonSubTypes by adding extra attributes to the above JSON i.e. type but the spec doesn't allow me to add that.
How can the appropriate concrete class could be instantiated based on the JSON above without altering?
Option 1: custom deserializer. Algorithm can be as follows:
Parse to JsonNode.
Use the properties in the node to find the correct class to deserialize into.
Convert the node to instance of the actual class.
Simplified example:
public class ValueDeserializer extends StdDeserializer<Value> {
public ValueDeserializer() {
super(Value.class);
}
#Override
public Value deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode root = parser.readValueAsTree();
if (root instanceof ObjectNode objectNode) {
JsonNode valueNode = objectNode.get("somePropertyName");
Class<? extends Value> clazz = valueNode == null ? RangeValue.class : FileValue.class;
return context.readTreeAsValue(objectNode, clazz);
}
throw new JsonParseException(parser, "not an object");
//handling the case, when json is json array
//or something else which can't be deserialized into object
}
}
Register the deserializer with JsonDeserialize on the interface:
#JsonDeserialize(using = ValueDeserializer.class)
Put the same annotation on RangeValue and FileValue, without specifying a deserializer, otherwise you will get StackOverflowError.
Option 2: use JsonTypeInfo.Id.DEDUCTION
#JsonSubTypes({
#JsonSubTypes.Type(FileValue.class),
#JsonSubTypes.Type(RangeValue.class)
})
#JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
public interface Value {
}
Jackson will deduce the correct class using the property names. Keep in mind exception will be thrown if it fails deduction.
Means that no serialized typing-property is used. Types are deduced based on the fields available. Deduction is limited to the names of fields (not their values or, consequently, any nested descendants). Exceptions will be thrown if not enough unique information is present to select a single subtype.
If deduction is being used annotation properties visible, property and include are ignored.
Am trying to deserialize a complex JSON structure using GSON. The API provider complicates things by providing an array in the results with a random name.
This is the (simplified/generified) JSON:
{
"field_1": "value",
"field_2": "value",
"field_3": {
"RANDOM_NAME": [
{
"array_field_1": "value",
"array_field_2": "value",
"array_field_3": "value"
},
{
"array_field_1": "value",
"array_field_2": "value",
"array_field_3": "value"
}
]
},
"field_4": "value"
}
and this is the corresponding (highly simplified) POJO:
public class responseObject {
String field_1;
String field_2;
Field3 field_3;
String field_4;
class Field3{
ArrayObject[] arrayObjects;
}
class ArrayObject{
String array_field_1;
String array_field_2;
String array_field_3;
}
}
However, when i run responseObject response = new Gson().fromJson(getJSON(),responseObject.class); i get the following call stack:
indicating that field_3 was not properly deserialized and does not contain an array of ArrayObject.
In this post the answers reference how to convert the data to a map, but in my case the data structure of each item in the array is actually much larger than this simplified example, and it defeats the purpose of using GSON if i have to manually pick the data i need out of a complex list of nested maps. also having trouble getting these answers to work in my scenario where the random object is an array an not a plain json object.
how do i get the randomly named array in the JSON to properly deserialize into the variable responseObject.Field3.arrayObjects??
You can avoid the complexity of using a TypeAdapeter by making the type of field_3 Map<String, List<ArrayObject>>
public class responseObject {
String field_1;
String field_2;
Map<String, List<ArrayObject>> field_3;
String field_4;
class ArrayObject{
String array_field_1;
String array_field_2;
String array_field_3;
}
}
And then to get the first item out of the Map without knowing its key you can use:
public List<ResponseObject.ArrayObject> getFirstValue(Map<String, List<ResponseObject.ArrayObject>> field_3) {
return field_3.values().iterator().next();
}
This can be solved by writing a custom TypeAdapter for Field3 which ignores the name of the property and only reads the value. The TypeAdapter has to be created by a TypeAdapterFactory to allow getting the delegate adapter for ArrayObject[]:
class Field3TypeAdapterFactory implements TypeAdapterFactory {
public Field3TypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// Only support Field3 class
if (type.getRawType() != Field3.class) {
return null;
}
TypeAdapter<ArrayObject[]> fieldValueAdapter = gson.getAdapter(ArrayObject[].class);
// Cast is safe, check at beginning made sure type is Field3
#SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<Field3>() {
#Override
public void write(JsonWriter out, Field3 value) throws IOException {
throw new UnsupportedOperationException("Serialization is not supported");
}
#Override
public Field3 read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
in.beginObject();
// Skip the random property name
in.skipValue();
ArrayObject[] fieldValue = fieldValueAdapter.read(in);
in.endObject();
Field3 object = new Field3();
object.arrayObjects = fieldValue;
return object;
}
};
return adapter;
}
}
You can then either register the factory with a GsonBuilder, or you can annotate your Field3 class with #JsonAdapter. When using #JsonAdapter the factory class should have a no-args constructor.
I am trying to deserialize JSON into a Java POJO using Jackson.
The Json looks like
"foo": {
"one": {
"a":1,
"b":"string"
}
"three":{
"a":2
"b":"another"
}
...
}
And the class I want to deserialize into has this field:
public class Myclass {
private Map<MyEnum, MyPojo> foo;
//setter and getter
public static MyPojo {
private int a;
private String b;
}
}
And my enum type looks like this:
public enum MyEnum {
one("data1"),two("data2")
#JsonValue
String data;
EnumAttrib(String data) {
this.data = data;
}
private static Map<String, MyEnum> ENUM_MAP = new HashMap();
static {
for (MyEnum a: MyEnum.values()) {
ENUM_MAP.put(a.data, a);
}
}
#JsonCreator
public static MyEnum fromData(String string) {
return ENUM_MAP.get(string);
}
}
This solution works well as long us the JSON has known keys which are captured by MyEnum. How can I skip certain JSON elements from serialization (in this example "three"), if it's not defined in MyEnum
You need to enable READ_UNKNOWN_ENUM_VALUES_AS_NULL which is disabled by default on your ObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
When you do this, the result Map will contain one entry with key equals to null and value set to JSON object related with one of unknown fields. If it is not acceptable you need to write custom deserialiser or remove null key after deserialisation process.
To solve problem with null key in Map you can also use EnumMap in your POJO:
private EnumMap<MyEnum, MyPojo> foo;
where null keys are not permitted and will be skipped.
See also:
Jackson deserializing into Map with an Enum Key, POJO Value
To solve your requirement, in case you are using Spring Boot, add this to your application.properties:
spring.jackson.deserialization.READ_UNKNOWN_ENUM_VALUES_AS_NULL=true
I've got the following enum:
public enum NotificationType {
Store("S"),
Employee("E"),
Department("D"),
All("A");
public String value;
NotificationType(String value) {
this.value = value;
}
#Override
public String toString() {
return this.value;
}
#JsonCreator
public static NotificationType fromValue(String value) {
for (NotificationType type : NotificationType.values()) {
if (type.value.equals(value)) {
return type;
}
}
throw new IllegalArgumentException();
}
}
I've created a converter so that when the enum is saved to the database, it persists the value (S, E, D or A) instead of the name. And I can POST json to the controller with the value and it binds to the object correctly.
However, when I render the JSON from a GET it is still displaying the name (Employee, Store, etc) and I would prefer that it still show the value.
Because your toString method returns the value you want to use to represent your enum, you can annotate it with #JsonValue to tell Jackson that the return value represents the value of the enum.
I have the following Enum:
public enum MyState {
Open("opened"),
Close("closed"),
Indeterminate("unknown");
private String desc;
private MyState(String desc) {
setDesc(desc);
}
public String getDesc() {
return this.desc;
}
private void setDesc(String desc) {
this.desc = desc;
}
}
I am trying to write an XStream Converter that will know to map back a JSON element "mystate" to a MyState instance.
"someJson": {
"object1": {
"mystate": closed
}
}
This should produce, amongst other objects (someJson and object1) a MyState.Close instance. I've started the Converter, but haven't gotten very far:
public class MyStateEnumConverter implement Converter {
#Override
public boolean canConvert(Class clazz) {
return clazz.equals(MyState.class);
}
#Override
public void marshal(Object value, HierarchialStreamWriter writer, MarshallingContext context) {
??? - no clue here
}
#Override
public Object unmarshal(HierarchialStreamReader reader, UnmarshallingContext context) {
??? - no clue here
}
}
Then, to create the mapper and use it:
XStream mapper = new XStream(new JettisonMappedXmlDriver());
mapper.registerConverter(new MyStateEnumConverter);
SomeJson jsonObj = mapper.fromXML(jsonString);
// Should print "closed"
System.out.println(jsonObject.getObject1().getMyState().getDesc());
How can I implement marshal and unmarshal so thatI get the desired mapping? Thanks in advance!
You can accomplish this by doing 2 things:
Adding a lookup method as well as a toString() override to your enum (MyStateEnum); and
Extending XStream's AbstractSingleValueConverter instead of implementing Converter
MyStateEnum:
public enum MyStateEnum {
// Everything you had is fine
// But now, add:
public static MyStateEnum getMyStateByDesc(String desc) {
for(MyStateEnum myState : MyStateEnum.values())
if(myState.getDesc().equals(desc))
return myState;
return null;
}
#Override
public String toString() {
return getDesc();
}
}
MyStateEnumConverter:
public class MyStateEnumConverter extends AbstractSingleValueConverter {
#Override
public boolean canConvert(Class clazz) {
return clazz.equals(MyStateEnum.class);
}
#Override
public Object fromString(String parsedText) {
return MyStateEnum.getMyStateByDesc(parsedText);
}
}
By adding getMyStateByDesc(String) to your enum, you now have a way to look up all the various enumerated values from the outside, by providing a desc string. The MyStateEnumConverter (which extends AbstractSingleValueConverter) uses your toString() override under the hood to associate aMyStateEnum instance with a text string.
So when XStream is parsing the JSON, it sees a JSON object of, say, "opened", and this new converter knows to pass "opened" into the converter's fromString(String) method, which in turn uses getMyStateByDesc(String) to lookup the appropriate enum instance.
Don't forget to register your converter with your XStream instance as you already showed in your original question.
You can use the EnumToStringConverter
Documentation
Example
#XStreamConverter(EnumToStringConverter.class)
public enum MyStateEnum {
enter code here
...
Use xstream.autodetectAnnotations(true)
Why are you using xstream for json support? You have a couple of other libraries specialized in json and that do it well. Also closed without quotes is not valid json.
Try for example Genson, it will work out of the box.
The values in the json stream would be "Close", "Indeterminate", etc and when deserializing it will produce the correct enum.
class SomeObject {
private MyState state;
...
}
Genson genson = new Genson();
// json = {"state" : "Indeterminate"}
String json = genson.serialize(new SomeObject(MyState.Indeterminate));
// deserialize back
SomeObject someObject = genson.deserialize(json, SomeObject.class);
// will print unknown
System.out.println(someObject.getDesc());