Currently, I'm receiving this JSON input, which I have no control whatsoever:
{
"A" : {
"B" : {
"B" : [{
"Whatever" : "String",
"Number": 123
}
],
"SiblingObject" : true
}
}
}
Basically, I want to deserialize the B array that's inside the B object directly into the A class without having to create another extra class to wrap the B object. Something like this:
public class A {
private List<B> bList;
public List<B> getB() {
return bList;
}
#JsonProperty("B")
public void setB(List<B> bList) {
this.bList = bList;
}
}
I've tried doing
public class A {
private List<B> bList;
public List<B> getB() {
return bList;
}
#JsonProperty("B")
public void setB(Map<String, Object> bList) {
this.bList = (List<B>) bList.get("B");
}
}
but to no avail.
Any ideas?
There is one way of doing it. However, it will require traversing the input JSON twice.
In first pass, you create the normal A instance without the List.
In second pass, you use Jackson's node traversal to reach the correct B object and parse from there.
See the code below:
public class WrapperJsonTest {
public static void main(String[] args) {
ObjectMapper om = new ObjectMapper();
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("wrapper.json");
A a = null;
try {
a = om.readValue(in, A.class);
} catch (Exception e){
e.printStackTrace();
}
in = Thread.currentThread().getContextClassLoader().getResourceAsStream("wrapper.json");
try {
JsonNode node = om.readValue(in, JsonNode.class).get("B").get("B");
JsonParser parser = node.traverse();
List<B> bList = om.readValue(parser, List.class);
a.setB(bList);
System.out.println(a.isSibling());
System.out.println(a.getB());
} catch (Exception e){
e.printStackTrace();
}
}
#JsonIgnoreProperties
public static class A {
#JsonIgnore
private List<B> bList;
private boolean sibling;
public List<B> getB() {
return bList;
}
public void setB(List<B> bList) {
this.bList = bList;
}
public boolean isSibling() {
return sibling;
}
public void setSibling(boolean sibling) {
this.sibling = sibling;
}
}
public static class B {
private String whatever;
public String getWhatever() {
return whatever;
}
public void setWhatever(String whatever) {
this.whatever = whatever;
}
#Override
public String toString() {
return whatever;
}
}
}
Related
For example I have two simple as possible classes, A and B
I want to take some action on objects of B, if some specific field of A object is changed I should do one thing, If some other field is changed I should do second thing, how can I do that with Lambda?
A:
public class A {
private int someField;
private String nextField;
public A(int someField, String nextField) {
this.someField = someField;
this.nextField = nextField;
}
public int getSomeField() {
return someField;
}
public void setSomeField(int someField) {
this.someField = someField;
}
public String getNextField() {
return nextField;
}
public void setNextField(String nextField) {
this.nextField = nextField;
}
}
B:
public class B {
private String someField;
public String getSomeField() {
return someField;
}
public void setSomeField(String someField) {
this.someField = someField;
}
public B(String someField) {
this.someField = someField;
}
}
Demo:
public class Demo {
public static <T> boolean isFieldChanged(T oldValue, T newValue) {
return !Objects.equals(oldValue, newValue);
}
public static void someActionOne(B test){
return;
}
public static void someActionTwo(B test){
return;
}
public static void main(String[] args) {
A oldData = new A(35, "old");
A clientData = new A(25, "ClientData");
Consumer<B> action = null;
if (isFieldChanged(oldData.getNextField(), clientData.getNextField())) {
action = Demo::someActionOne;
} else if (isFieldChanged(oldData.getSomeField(), clientData.getSomeField())) {
action = Demo::someActionTwo;
}
List<B> mockData = new ArrayList<>(Arrays.asList(new B("test1"), new B("test2")));
mockData.forEach(b -> action.accept(b));
}
}
How can I avoid compile error in that case?
To be effectively-final, a variable must not be changed after initialization.
If you want to use different actions, just initialize them twice:
public static void main(String[] args) {
A oldData = new A(35, "old");
A clientData = new A(25, "ClientData");
List<B> mockData = new ArrayList<>(Arrays.asList(new B("test1"), new B("test2")));
if (isFieldChanged(oldData.getNextField(), clientData.getNextField())) {
mockData.forEach(Demo::someActionOne);
} else if (isFieldChanged(oldData.getSomeField(), clientData.getSomeField())) {
mockData.forEach(Demo::someActionTwo);
}
}
I am having issues when trying to deserializing the following class:
public class MetricValuesDto {
private Map<MetricType, MetricValueDto<?>> metricValues;
public MetricValuesDto() {
}
public MetricValuesDto(Map<MetricType, MetricValueDto<?>> metricValues) {
this.metricValues = metricValues;
}
public Map<MetricType, MetricValueDto<?>> getMetricValues() {
return metricValues;
}
public void setMetricValues(Map<MetricType, MetricValueDto<?>> metricValues) {
this.metricValues = metricValues;
}
}
My generic abstract class:
public abstract class MetricValueDto<T> {
private T value;
private MetricTrend trend;
public MetricValueDto(T value, MetricTrend trend) {
this.value = value;
this.trend = trend;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public MetricTrend getTrend() {
return trend;
}
public void setTrend(MetricTrend trend) {
this.trend = trend;
}
}
I have two concrete classes which implement MetricValueDto:
IntMetricValueDto:
public class IntMetricValueDto extends MetricValueDto<Integer> {
public IntMetricValueDto(Integer value, MetricTrend trend) {
super(value, trend);
}
}
FloatMetricValueDto:
public class FloatMetricValueDto extends MetricValueDto<Float> {
public FloatMetricValueDto(Float value, MetricTrend trend) {
super(value, trend);
}
}
Any idea of what's the correct strategy to deserialize MetricValueDto so I can parse it through ObjectMapper or an RestTemplate? Whenever I run:
restTemplate.exchange("myEndpoint", HttpMethod.GET, entity, DataCollectionEventDto.class);
I get
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.resson.dto.MetricValueDto: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
DataCollectionEventDto:
public class DataCollectionEventDto {
private List<MapLayerDto> mapLayers;
#JsonUnwrapped
private MetricValuesDto metricValues;
public List<MapLayerDto> getMapLayers() {
return mapLayers;
}
public void setMapLayers(List<MapLayerDto> mapLayers) {
this.mapLayers = mapLayers;
}
public MetricValuesDto getMetricValues() {
return metricValues;
}
public void setMetricValues(MetricValuesDto metricValues) {
this.metricValues = metricValues;
}
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
I have basically tried everything on web and I could not make it work; any suggestion would be helpful.
Use JsonSubTypes annotation with JsonTypeInfo to indicate subtypes. The property attribute JsonTypeInfo is used to differentiate between different subclasses.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "typ")
#JsonSubTypes({
#JsonSubTypes.Type(value = IntMetricValueDto.class, name = "INT"),
#JsonSubTypes.Type(value = FloatMetricValueDto.class, name = "FLT")})
public abstract class MetricValueDto<T> {
private T value;
private MetricTrend trend;
...
}
While JsonTypeInfo works, and adds implementation-specific detail to the response, which later might add confusion to the API client.
I ended up implementing a custom StdDeserializer:
public class MetricValueDtoDeserializer<T> extends StdDeserializer<MetricValueDto<T>> {
private static final long serialVersionUID = 1L;
public MetricValueDtoDeserializer() {
this(null);
}
public MetricValueDtoDeserializer(Class<?> vc) {
super(vc);
}
private ObjectMapper mapper;
#Override
public MetricValueDto<T> deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
String metricType = jsonParser.getCurrentName();
mapper = (ObjectMapper) jsonParser.getCodec();
ObjectNode objectNode = (ObjectNode) mapper.readTree(jsonParser);
Iterator<Entry<String, JsonNode>> elementsIterator = objectNode.fields();
Number number = null;
while (elementsIterator.hasNext()) {
Entry<String, JsonNode> element = elementsIterator.next();
String key = element.getKey();
if (key.equals("value")) {
number = parseValue(element, metricType);
}
if (key.equals("trend")) {
MetricTrend metricTrend = parseTrend(element);
return (produceMetricValueDto(number, metricTrend));
}
}
throw new IOException();
}
#SuppressWarnings("unchecked")
private MetricValueDto<T> produceMetricValueDto(Number number, MetricTrend metricTrend) throws IOException {
if (number instanceof Integer) {
return (MetricValueDto<T>) new IntMetricValueDto((Integer) number, metricTrend);
} else if (number instanceof Float) {
return (MetricValueDto<T>) new FloatMetricValueDto((Float) number, metricTrend);
}
throw new IOException();
}
private MetricTrend parseTrend(Entry<String, JsonNode> element)
throws JsonProcessingException {
String trend = mapper.treeToValue(element.getValue(), String.class);
if (trend == null) {
return null;
} else {
return MetricTrend.valueOf(trend);
}
}
private Number parseValue(Entry<String, JsonNode> element, String metricType)
throws IOException {
if (metricType.equals(MetricType.CANOPY_COVERAGE.toValue())
|| metricType.equals(MetricType.PLANT_SIZE.toValue())) {
return mapper.treeToValue(element.getValue(), Float.class);
} else if (metricType.equals(MetricType.INSECT_COUNT.toValue())
|| metricType.equals(MetricType.PLANT_COUNT.toValue())) {
return mapper.treeToValue(element.getValue(), Integer.class);
}
throw new IOException();
}
}
The code ended up to being more complex than JsonTypeInfo, but the API client is unaware of implementation-specific details.
I have this json taat I'm trying to convert to an aobject of type DataStreams :
{
"resultSize": "14",
"requestedSize": "1000",
"pageCursor": "c8bdb3d9-e-ccecb4a7",
"items": [
{ "cstId": "5146"}]}
and I have this class :
#JsonIgnoreProperties(ignoreUnknown = true)
public class DataStreams {
private String resultSize;
private String requestedSize;
private String pageCursor;
private Item[] items;
public String getResultSize() {
return resultSize;
}
public void setResultSize(String resultSize) {
this.resultSize = resultSize;
}
public String getRequestedSize() {
return requestedSize;
}
public void setRequestedSize(String requestedSize) {
this.requestedSize = requestedSize;
}
public String getPageCursor() {
return pageCursor;
}
public void setPageCursor(String pageCursor) {
this.pageCursor = pageCursor;
}
public Item[] getDataStreams() {
return items;
}
public void setDataStreams(Item[] items) {
this.items = items;
}
}
then I do this in my application class :
DataStreams dataStreams = mapper.readValue(str, DataStreams.class);
but the items array is getting null.
How do I get the items array ?
The Gist
I tried to deserialize some JSON text with GSON. The JSON string had values defined. However, the deserialized string has null values.
Exactly what I did
I tried to deserialize some JSON text with GSON
SomeSpec deserializedJson = GSON.fromJson(serializedJson, SomeSpec.class);
where serializedJson is a string containing
{
"some_class": "abc.def.SomeClass",
"stuff": [
"FOO",
"BAR",
],
"definition": {
"values": [
{ "feature": "FOO", "value": 1.0 },
{ "feature": "BAR", "value": 1.0 },
]
}
}
and SomeSpec is a java class containing:
package somepackagepath;
import java.util.List;
public class SomeSpec {
private List<FeatureValueSpec> _values;
private List<String> _postProcessFunctions;
public List<FeatureValueSpec> getValues() {
return _values;
}
public List<String> getPostProcessFunctions() {
return _postProcessFunctions;
}
public static class FeatureValueSpec {
private String _feature;
private float _value;
public String getFeature() {
return _feature;
}
public float getValue() {
return _value;
}
}
}
The deserialized object had only null fields even though the JSON clearly had those fields defined.
First: There are two errors in your JSON in Arrays. There are extra commas at end of each array.
Second your models should look like this
public class Values
{
private String value;
private String feature;
public String getValue ()
{
return value;
}
public void setValue (String value)
{
this.value = value;
}
public String getFeature ()
{
return feature;
}
public void setFeature (String feature)
{
this.feature = feature;
}
}
public class Definition
{
private Values[] values;
public Values[] getValues ()
{
return values;
}
public void setValues (Values[] values)
{
this.values = values;
}
}
public class MyPojo
{
private Definition definition;
private String[] stuff;
private String some_class;
public Definition getDefinition ()
{
return definition;
}
public void setDefinition (Definition definition)
{
this.definition = definition;
}
public String[] getStuff ()
{
return stuff;
}
public void setStuff (String[] stuff)
{
this.stuff = stuff;
}
public String getSome_class ()
{
return some_class;
}
public void setSome_class (String some_class)
{
this.some_class = some_class;
}
}
Mapping an enum class in to DynamoDB object is really simple by using Custom Marshall. But how to map a List of Enum?
Enum class
public enum Transport {
SMS,EMAIL,ALL;
}
DynamoDB mapper
public class Campaign{
private List<Transport> transport;
#DynamoDBAttribute(attributeName = "transport")
public List<Transport> getTransport() {
return transport;
}
public void setTransport(List<Transport> transport) {
this.transport = transport;
}
}
DynamoDBMarshaller is deprecated.
Use DynamoDBTypeConverter instead.
Example:
Enum class
public static enum ValidationFailure {
FRAUD, GENERAL_ERROR
}
DynamoDBTable class
#DynamoDBTable(tableName = "receipt")
public class Receipt {
private Long id;
private List<ValidationFailure> validation;
#DynamoDBHashKey(attributeName = "id")
public Long getId() {
return id;
}
#DynamoDBTypeConverted(converter = ValidationConverter.class)
public List<ValidationFailure> getValidation() {
return validation;
}
public void setId(Long id) {
this.id = id;
}
public void setValidation(List<ValidationFailure> validation) {
this.validation = validation;
}
}
Convertor:
public class ValidationConverter implements DynamoDBTypeConverter<List<String>, List<ValidationFailure>> {
#Override
public List<String> convert(List<ValidationFailure> object) {
List<String> result = new ArrayList<String>();
if (object != null) {
object.stream().forEach(e -> result.add(e.name()));
}
return result;
}
#Override
public List<ValidationFailure> unconvert(List<String> object) {
List<ValidationFailure> result = new ArrayList<ValidationFailure>();
if (object != null) {
object.stream().forEach(e -> result.add(ValidationFailure.valueOf(e)));
}
return result;
}
}
It's working for me, I have used the Set
#DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.SS)
var roles: MutableSet<Employee.Role>? = null
I think the same approach would work for List with DynamoDBAttributeType.L
I found the answer myself. I create a custom marshall like below.
public class TransportMarshaller implements DynamoDBMarshaller<List<Transport>> {
#Override
public String marshall(List<Transport> transports) {
List<String>transportMap=new ArrayList<>();
for(Transport transport:transports){
transportMap.add(transport.name());
}
return transportMap.toString().replaceAll("\\[|\\]", "");//Save as comma separate value for the purpose of easiness to unmarshall
}
#Override
public List<Transport> unmarshall(Class<List<Transport>> aClass, String s) {
List<String>map= Arrays.asList(s.split("\\s*,\\s*")); //split from comma and parse to List
List<Transport>transports=new ArrayList<>();
for (String st:map){
transports.add(Transport.valueOf(st));
}
return transports;
}
}