I try to use only immutables objects in my application. I've got a REST service that will take arbitrary JSon objects as input.
I've a Java class that map theses objects, and I want to make them immutable + able to deal with extra parameters (just like using #JsonAnySetter).
Here is my java class:
public class Operation {
private final String _id;
private final String state;
private final Map<String, Object> extra;
public Operation(String _id, String state, Map<String,Object> extra) {
this._id = _id;
this.state = state;
this.extra = extra;
}
// getters....
}
Using #JsonAnySetter I would have:
public class Operation {
private final String _id;
private final String state;
private Map<String, Object> extra = new HashMap<>();
public Operation(String _id, String state) {
this._id = _id;
this.state = state;
}
#JsonAnySetter
public void addExtra(String key, Object value) {
this.extra.put(key,value);
}
// getters....
}
But this is not immutable anymore !
This will not work because Jackson do not find any "extra" json attribute to read. I would like that everything that cannot be mapped be added to my map.
Any idea of how to do this ? (or is it just possible :)
Note: I use javac with -parameters option and the ParameterNameModule from jackson so that I don't need #JsonCreator option.
Ok so I respond to myself :)
It seems that it is not possible to do that using only Jackson.
Because I want immutability, I've turned myself to the 'immutables' framework: http://immutables.github.io/
With a little configuration, it will deal with extra parameters as stated in the following report: https://github.com/immutables/immutables/issues/185.
In my situation, I've got the following code:
#Value.Immutable
#JsonSerialize(as = ImmutableOperation.class)
#JsonDeserialize(as = ImmutableOperation.class)
public abstract class Operation {
#JsonAnyGetter
#Value.Parameter
public abstract Map<String, String> extra();
}
Refer to the documentation of immutables for the details.
If you want to deserialize immutable entity with extra arguments you can utilize builder pattern:
#JsonPOJOBuilder
public class OperationBuilder {
private String _id;
private String _state;
private Map<String, Object> extra = new HashMap<>();
#JsonAnySetter
public OperationBuilder addExtra(String key, Object value) {
this.extra.put(key,value);
return this;
}
// setters....
public Operation build() {
return new Operation(...arguments...)
}
And your original class should have this annotation on a class level:
#JsonDeserializer(builder = OperationBuilder.class)
This way all your known and unknown (extra) fields will be populated inside the builder and then Jackson will call build() method at the end of the deserialization.
Related
I am trying to see if I can replace my existing Pojos with the new Record classes in Java 14. But unable to do so. Getting following error:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of com.a.a.Post (no Creators, like default
construct, exist): cannot deserialize from Object value (no delegate-
or property-based Creator)
I get that the error is saying the record has no constructors, but from what I see the record class takes care of it in the background and relevant getters are also set in the background (not getters exactly but id() title() and so on without the get prefix). Is it cos Spring has not adopted the latest Java 14 record yet? Please advice. Thanks.
I am doing this in Spring Boot version 2.2.6 and using Java 14.
The following works using the usual POJOs.
PostClass
public class PostClass {
private int userId;
private int id;
private String title;
private String body;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
Method to call rest service which works now as I am using the above POJO.
public PostClass[] getPosts() throws URISyntaxException {
String url = "https://jsonplaceholder.typicode.com/posts";
return template.getForEntity(new URI(url), PostClass[].class).getBody();
}
But if I switch to following where I am using record instead, I am getting the above error.
The new record class.
public record Post(int userId, int id, String title, String body) {
}
Changing the method to use the record instead which fails.
public Post[] getPosts() throws URISyntaxException {
String url = "https://jsonplaceholder.typicode.com/posts";
return template.getForEntity(new URI(url), Post[].class).getBody();
}
EDIT:
Tried adding constructors as follows to the record Post and same error:
public record Post(int userId, int id, String title, String body) {
public Post {
}
}
or
public record Post(int userId, int id, String title, String body) {
public Post(int userId, int id, String title, String body) {
this.userId = userId;
this.id = id;
this.title = title;
this.body = body;
}
}
It is possible with some Jackson Annotations, which cause Jackson to use fields instead of getters. Still far less verbose than a pre-Java 14 class (without Lombok or similar solutions).
record Foo(#JsonProperty("a") int a, #JsonProperty("b") int b){
}
This probably works because according to https://openjdk.java.net/jeps/359:
Declaration annotations are permitted on record components if they are
applicable to record components, parameters, fields, or methods.
Declaration annotations that are applicable to any of these targets
are propagated to implicit declarations of any mandated members.
See also: When is the #JsonProperty property used and what is it used for?
It is also possible to make use #JsonAutoDetect
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
record Bar(int a, int b){
}
If configuring the Objectmapper to use field Visibility globally, this annotation on class level is not needed.
See also: How to specify jackson to only use fields - preferably globally
Example:
public class Test {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(new Foo(1, 2))); //{"a":1,"b":2}
System.out.println(om.writeValueAsString(new Bar(3, 4))); //{"a":3,"b":4}
}
record Foo(#JsonProperty("a") int a, #JsonProperty("b") int b){
}
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
record Bar(int a, int b){
}
}
There is also a Github issue for that feature: https://github.com/FasterXML/jackson-future-ideas/issues/46
This is slated for jackson 2.12
https://github.com/FasterXML/jackson-future-ideas/issues/46
The compiler generates the constructor and other accessor method for a Record.
In your case,
public final class Post extends java.lang.Record {
public Post(int, int java.lang.String, java.lang.String);
public java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public int userId();
public int id();
public java.lang.String title();
public java.lang.String body();
}
Here you can see that there is not default constructor which is needed got Jackson. The constructor you used is a compact constructor,
public Post {
}
You can define a default/no args constructor as,
public record Post(int userId, int id, String title, String body) {
public Post() {
this(0,0, null, null);
}
}
But Jackson uses Getter and Setters to set values. So in short, you can not use Record for mapping the response.
EDIT as PSA: Jackson can properly serialize and deserialize records as of 2.12 which has been released.
Use the parameter names module for jackson, https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names (make sure the compiler sets -parameters) or add `#JsonProperty("name") to each field in the record
add #JsonCreator to the constructor. I can't tell if the inheritance will work properly, so you might have to explicitly declare the constructor and annotate it.
If a public accessor method or (non-compact) canonical constructor is declared explicitly, then it only has the annotations which appear on it directly; nothing is propagated from the corresponding record component to these members.
From https://openjdk.java.net/jeps/384
So add
new ObjectMapper().registerModules(new ParameterNamesModule())
and try
#JsonCreator record Value(String x);
or something like
record Value(String x) {
#JsonCreator
public Value(String x) {
this.x = x;
}
}
or all the way to
record Value(#JsonProperty("x") String x) {
#JsonCreator
public Value(#JsonProperty("x") String x) {
this.x = x;
}
}
This is how I get immutable pojos with lombok and jackson to work, and I don't see why records wouldn't work under the same format. My setup is Jackson parameter names module, -parameters compiler flag for java 8 (I don't think this is required for like jdk9+), #JsonCreator on the constructor. Example of a real class working with this setup.
#Value
#AllArgsConstructor(onConstructor_ = #JsonCreator)
public final class Address {
private final String line1;
private final String line2;
private final String city;
private final String region;
private final String postalCode;
private final CountryCode country;
}
I use ModelMapper in my project to map between DTO classes and models.
For example:
public class UserDto {
private String name;
private String phone;
private String email;
}
public class User {
#Id
private String id;
private String metaDatal;
private String name;
private String phone;
private String email;
}
Here How I map it:
#Autowired
private ModelMapper modelMapper;
modelMapper.map(userDto, user);
As you can see I have metaDatal field in the user model, I want to set this field with a specific value.
Specific field(metaDatal) of the mapped class I want to set this value "abc123".
Is there any way to tell map method when it called that, specific filed(for example metaData) should have specific value(for example abc123)?
I believe the most flexible way to do this is to implement a simple Converter. Check this:
Converter<UserDto, User> metaData = new Converter<UserDto, User>() {
// This is needed to convert as usual but not having not registered
// this converter to avoid recursion
private final ModelMapper mm = new ModelMapper();
#Override
public User convert(MappingContext<UserDto, User> context) {
User u = context.getDestination();
mm.map(context.getSource(), u);
u.setMetaDatal("abc123");
return context.getDestination();
}
};
Now it is just to create a TypeMap and set this converter to handle conversion, like:
modelMapper.createTypeMap(UserDto.class, User.class).setConverter(metaData);
before modelMapper.map().
You could also add a getter for metadata in your UserDto, like:
public String getMetaDatal() {
return "abc123";
}
If it is something that can be derived from UserDto directly and skip the converter part.
For a school assignment, I need to parse a CSV into a Bean and present it in a JavaFX GUI later. I decided to use the Library opencsv, which worked fine.
But now, I would like to parse the attributes directly into SimpleObjectProperties. How do I do that? Unfortunately, I couldn't find any further information.
Code looks like this:
public class Phone {
#CsvBindByName(column = "ENTITY_ID")
private SimpleIntegerProperty entityId;
#CsvBindByName(column = "OPERATING_COMPANY")
private SimpleStringProperty operatingCompany;
When I run the code, I get a CsvDataTypeMismatchException (Conversion of 1006 to javafx.beans.property.SimpleIntegerProperty failed).
Any help much appreciated, thank you!!
Looking at the documentation it looks like you can create CustomConverts for each type of property you have; the example they have on the documentation page, this is the start to an IntegerPropertyConverter.
public class IntegerPropertyConverter extends AbstractCsvConverter {
#Override
public Object convert(String value) {
return new SimpleIntegerProperty(Integer.parseInt(value));
}
#Override
public String convertToWrite(Object value) {
IntegerProprety prop = (IntegerProperty) value;
return String.format("%d", prop.get());
}
}
Then you'd use:
#CsvCustomBindByName(column = "ENTITY_ID", converter = IntegerPropertyConverter.class)
private SimpleIntegerProperty entityId;
If you need to create your properties using the longer format, you will need to override other methods in the AbstractBeanField, such as public final void setFieldValue(T bean, String value, String header) where you can actually use the bean to create the
There is no easy way around this.
You keep your Phone a POJO and map the entire object as a property
private SimpleObjectProperty<Phone> phone = new SimpleObjectProperty<Phone>();
Or you could add properties to the Phone
public class Phone {
#CsvBindByName(column = "ENTITY_ID")
private Integer entityId;
private final SimpleIntegerProperty entityIdProperty;
public Phone() {
entityIdProperty = new SimpleIntegerProperty();
entityIdProperty.addListener((o, oldValue,newValue)->{
entityId = newValue.intValue();
});
}
public Integer getEntityId() {
return entityId;
}
public void setEntityId(Integer entityId) {
this.entityId = entityId;
entityIdProperty.set(entityId);
}
public SimpleIntegerProperty getEntityIdProperty() {
return entityIdProperty;
}
// ...
}
If you do not need this object to have a bidirectional binding you could skip the listener.
There are other possibilities too, like having Methods and constructors to convert from a Phone to a PhoneFX (with properties instead of simple types) and viceversa.
I have some kind of job control with persistence via a database.
There is a interface for actions:
public interface IAction {
Object perform(Work work, Map<String, String> parameter) throws Exception;
}
There are multiple implementations:
public class SingleFileConvertAction implements IAction {
public InputStream perform(Work work, Map<String, String> parameter) throws Exception {
// ...
}
}
public class CacheDeleteAction implements IAction {
public Object perform(Work work, Map<String, String> parameter) throws Exception {
// ...
}
}
// ...
There is a job control class:
#Entity
public class ActionControl {
#Id
#GeneratedValue
private Integer id;
#ElementCollection
private Map<String, String> parameter = new HashMap<String, String>();
#ManyToOne
#JoinColumn(name = "work_id", referencedColumnName = "id", nullable = false)
private Work work;
private IAction action;
private Date requestTime;
private Date startTime;
private Date endTime;
// ...
private ActionControl() {}
public ActionControl(Work work, String action, Map<String, String> parameter) {
this.parameter = parameter;
this.work = work;
// ...
}
}
Now I want to save the action control to the database. I only need to know, which action class to use. Everything else is saved in work and parameter.
I thought about saving a string and do a switch() to choose it like "CacheDeleteAction" -> CacheDeleteAction but I assume there is a better way to do it. Is it possible to save "CacheDeleteAction.class" in a database field? (I saw it in Spring annotations)
How to save a reference to a java class in a database?
as #XtremeBaumer said depends on your usage, if you need to create an instance later on then what XtremeBaumer has suggested would be preferred way, else with enum it would go something like this,
public enum IActionEnum {
CACHE_DELETE_ACTION("Cache delete action"),
SINGLE_FILE_CONVERT_ACTION("Single file convert action"),
private String actionDescription;
IActionEnum(String description) {
actionDescription= description;
}
#Override
public String toString() {
return actionDescription;
}
}
And whenever you would like to save it to DB you can use it like,
IAction actionControl = new IAction();
// set all other parameters
actionControl.setIAction(IActionEnum.CACHE_DELETE_ACTION)
actionControlRepository.save(actionControl);
I'm trying to figure out how can I bind json to POJO.
When json can sometime contain additional fields depending on various conditions. Basically speaking - some part of json will always contain same properties,
for example: name and age. But sometimes I'll get shoeSize and/or eyeColor. I cant make list of all possible properties that can be passed to me, because some of them are defined by user.
Is there possibility to achieve something like this?
class MyClass
{
public String name;
public Integer age;
public Map<String, String> additionalArguments;
public MyClass(...) {...}
}
After going through documentation (again) I found annotation called JsonAnySetter and process reverting annotation JsonAnyGetter
class MyClass
{
public String name;
public Integer age;
public Map<String, String> additionalArguments = new HashMap<>();
public MyClass(...) {...}
#JsonAnyGetter
public Map<String,Object> getAdditionalProperties() {
return additionalProperties;
}
#JsonAnySetter
public void putAdditionalProperty(String name, Object value) {
additionalProperties.put(name, value);
}
}