Modelmapper is not recognizing UUIDs - java

Hey I just began playing around with Modelmapper to map jOOQ records to POJOs.
This is the schema for the table whose records I am attempting to convert (Postgresql)
CREATE TABLE IF NOT EXISTS actor(
actor_id UUID DEFAULT uuid_generate_v4(),
first_name VARCHAR(256) NOT NULL,
last_name VARCHAR(256) NOT NULL,
PRIMARY KEY(actor_id)
);
Here is what the POJO looks like:
#JsonDeserialize(builder = Actor.Builder.class)
public class Actor {
private final UUID actorId;
private final String firstName;
private final String lastName;
private Actor(final Builder builder) {
actorId = builder.actorId;
firstName = builder.firstName;
lastName = builder.lastName;
}
public static Builder newBuilder() {
return new Builder();
}
public UUID getActorId() {
return actorId;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
#JsonIgnoreProperties(ignoreUnknown = true)
public static final class Builder {
private UUID actorId;
private String firstName;
private String lastName;
private Builder() {
}
public Builder withActorId(final UUID val) {
actorId = val;
return this;
}
public Builder withFirstName(final String val) {
firstName = val;
return this;
}
public Builder withLastName(final String val) {
lastName = val;
return this;
}
public Actor build() {
return new Actor(this);
}
}
}
I am creating a ModelMapper bean in my application and registering a UUID converter to it.
#Bean
public ModelMapper modelMapper() {
final ModelMapper mapper = new ModelMapper();
Provider<UUID> uuidProvider = new AbstractProvider<UUID>() {
#Override
public UUID get() {
return UUID.randomUUID();
}
};
final Converter<String, UUID> uuidConverter = new AbstractConverter<>() {
#Override
protected UUID convert(final String source) {
return UUID.fromString(source);
}
};
mapper.createTypeMap(String.class, UUID.class);
mapper.addConverter(uuidConverter);
mapper.getTypeMap(String.class, UUID.class).setProvider(uuidProvider);
mapper.getConfiguration()
.setSourceNameTokenizer(NameTokenizers.UNDERSCORE)
.addValueReader(new RecordValueReader())
.setDestinationNameTransformer(NameTransformers.builder("with"))
.setDestinationNamingConvention(NamingConventions.builder("with"));
mapper.validate();
return mapper;
}
I then use the model mapper to map the ActorRecord from the jOOQ autogenerated code to the POJO
public Optional<Actor> getActor(final UUID actorId) {
return Optional.ofNullable(dsl.selectFrom(ACTOR)
.where(ACTOR.ACTOR_ID.eq(actorId))
.fetchOne())
.map(e -> modelMapper.map(e, Actor.Builder.class).build());
}
This works except the UUID is always null. For example:
{"actor_id":null,"first_name":"John","last_name":"Doe"}
However when I change the following in the Builder:
public Builder withActorId(final String val) {
actorId = UUID.fromString(val);
return this;
}
It works! Unfortunately this does not work with an overloaded method:
public Builder withActorId(final String val) {
actorId = UUID.fromString(val);
return this;
}
public Builder withActorId(final UUID val) {
actorId = val;
return this;
}
As this also returns null.
You can see from the autogenerated jOOQ code it should be handling a UUID:
/**
* The column <code>public.actor.actor_id</code>.
*/
public final TableField<ActorRecord, UUID> ACTOR_ID = createField(DSL.name("actor_id"), org.jooq.impl.SQLDataType.UUID.nullable(false).defaultValue(org.jooq.impl.DSL.field("uuid_generate_v4()", org.jooq.impl.SQLDataType.UUID)), this, "");
I am not sure what I am exactly missing. I do not want to create a custom converter for each of my entities as I have a lot of them and they all contain (at least 1) UUID. Ideally I want to configure the ModelMapper to know about UUID and whenever it sees one it can handle it. Thanks!
NOTE: I also tried this with Lombok #Data object and it does not work either.
#JsonDeserialize(builder = Actor.ActorBuilder.class)
#Data
public class Actor {
private UUID actorId;
private String firstName;
private String lastName;
#JsonPOJOBuilder(withPrefix = "with")
public static class ActorBuilder {
}
}

UUID.fromString(val) is not allowed. I had same problem yesterday. Try to put to ModelMapper configurations UUID converted to String.

Related

Spring Boot findById not working for MongoDB

I'm trying to do a simple get query on springboot using mongodb as database engine
I have tried with several stuff(sending the data as ObjectId and even changing the repository)
public ResponseEntity<Track> get(String trackId) {
Track find = mongoTemplate.findById(new ObjectId(trackId), Track.class);
Optional<Track> track = tracksRepository.findById(trackId);
if (track.isPresent()) {
return new ResponseEntity<>(track.get(), HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
with mongo config
#Configuration
#EnableMongoRepositories(basePackages = "data.store.repositories")
public class MongoConfig extends AbstractMongoClientConfiguration {
private final Logger LOGGER = Logger.getLogger(this.getClass().getSimpleName());
#Primary
#Bean
#Override
public MongoClient mongoClient() {
return MongoClients.create(MongoClientSettings.builder()
.applyToClusterSettings(builder -> builder.hosts(Arrays.asList(new ServerAddress(host, port))))
.build());
}
private MongoCredential mongoCredentials() {
return MongoCredential.createCredential(username, database, password.toCharArray());
}
#Bean
public MongoTemplate mongoTemplate() {
MongoTemplate mongoTemplate = new MongoTemplate(mongoClient(), getDatabaseName());
mongoTemplate.setReadPreference(ReadPreference.secondaryPreferred());
return mongoTemplate;
}
protected String getDatabaseName() {
return database;
}
#Override
public boolean autoIndexCreation() {
return false;
}
}
EDIT: Adding class for context
#Document("track")
public class Track {
#Id
#Field(ATTR_ID)
#JsonProperty(ATTR_ID)
public String id;
public static final String ATTR_ID = "id";
}
and getting always null, with existing keys on my database. could you help me find the issue?
Thanks in advance
I tried this with similar configuration class and found the following worked fine creating/accessing data using MongoTemplate.
The POJO class:
public class Test {
#MongoId(FieldType.OBJECT_ID)
private String id;
private String name;
public Test() {
}
public Test(String s) {
super();
this.name = s;
}
// get, set methods
public String toString( ) {
return id + " - " + name;
}
}
From Spring's CommandLineRunner.run():
// Insert a document into the database
Test t1 = new Test("alpha");
t1 = mt.insert(t1);
System.out.println(t1); // 61e7de9f5aadc2077d9f4a58 - alpha
// Query from the database using the _id
ObjectId id = new ObjectId("61e7de9f5aadc2077d9f4a58");
Test t2 = mt.findById(id, Test.class);
System.out.println(t2);
Note that you need to do this from the class where you are running the code:
#Autowired private MongoTemplate mt;
You can use the #MongoId or #Id annotations in our POJO class to represent MongoDB _id field. The type of the field can be a String or ObjectId. It depends upon how you define.
See this from Spring Data MongoDB documentation on How the _id Field is Handled in the Mapping Layer using:
#MongoId
#Id
Solution is to add to MongoId annotation field type object id
#MongoId(FieldType.OBJECT_ID)
private String id;

MongoDB repository save() do the insert instead of update

I am writing a PUT request API with spring and mongodb. But the save() inserts a new object instead of update the current one.
#Document("Test")
public class Expense {
#Field(name = "name")
private String expenseName;
#Field(name = "category")
private ExpenseCategory expenseCategory;
#Field(name = "amount")
private BigDecimal expenseAmount;
public Expense( String expenseName, ExpenseCategory expenseCategory, BigDecimal expenseAmount) {
this.expenseName = expenseName;
this.expenseCategory = expenseCategory;
this.expenseAmount = expenseAmount;
}
public String getExpenseName() {
return expenseName;
}
public void setExpenseName(String expenseName) {
this.expenseName = expenseName;
}
public ExpenseCategory getExpenseCategory() {
return expenseCategory;
}
public void setExpenseCategory(ExpenseCategory expenseCategory) {
this.expenseCategory = expenseCategory;
}
public BigDecimal getExpenseAmount() {
return expenseAmount;
}
public void setExpenseAmount(BigDecimal expenseAmount) {
this.expenseAmount = expenseAmount;
}
}
This is my reporsitory class
public interface ExpenseRepository extends MongoRepository<Expense, String> {
}
This is my Service class which shows how to update the class.
#Service
public class ExpenseService {
private final ExpenseRepository expenseRepository;
public ExpenseService(ExpenseRepository expenseRepository) {
this.expenseRepository = expenseRepository;
}
public void updateExpense(String id, Expense expense){
Expense savedExpense = expenseRepository.findById(id)
.orElseThrow(() -> new RuntimeException(
String.format("Cannot Find Expense by ID %s", id)));
savedExpense.setExpenseName(expense.getExpenseName());
savedExpense.setExpenseAmount(expense.getExpenseAmount());
savedExpense.setExpenseCategory(expense.getExpenseCategory());
expenseRepository.save(savedExpense);
}
}
This is my controller
#RestController
#RequestMapping("/api/expense")
public class ExpenseController {
private final ExpenseService expenseService;
public ExpenseController(ExpenseService expenseService) {
this.expenseService = expenseService;
}
#PutMapping("/{id}")
public ResponseEntity<Object> updateExpense(#PathVariable String id, #RequestBody Expense expense){
expenseService.updateExpense(id, expense);
return ResponseEntity.ok().build();
}
}
As shown in mongodb compass, mongodb auto generates an _id field for every object. So I do not define a id field or use #id annotation to define a primary for the collection. However, in the service class, expenseRepository.findById(id) retrieves the desired object and update it. Why does save() do the insert instead of update? Many thanks.
JPA Can't find the existing entry as no id field id set. You need to add an id field and set generation type to auto.
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;

How work with immutable object in mongodb and lombook without #BsonDiscriminator

I tried to work with immutable objects in MongoDB and Lombok. I found a solution to my problem but it needs to write additional code from docs but I need to used Bson annotations and create a constructor where describes fields via annotations. But if I user #AllArgsConstructor catch exception: "Cannot find a public constructor for 'User'" because I can't use default constructor with final fields. I think i can customize CodecRegistry correctly and the example will work correctly but I couldn't find solution for it in docs and google and Stackoverflow.
Is there a way to solve this problem?
#Data
#Builder(builderClassName = "Builder")
#Value
#BsonDiscriminator
public class User {
private final ObjectId id;
private final String name;
private final String pass;
private final String login;
private final Role role;
#BsonCreator
public User(#BsonProperty("id") final ObjectId id,
#BsonProperty("name") final String name,
#BsonProperty("pass") final String pass,
#BsonProperty("login") final String login,
#BsonProperty("role") final Role role) {
this.id = id;
this.name = name;
this.pass = pass;
this.login = login;
this.role = role;
}
#AllArgsConstructor
public enum Role {
USER("USER"),
ADMIN("ADMIN"),
GUEST("GUEST");
#Getter
private String value;
}
public static class Builder {
}
}
Example for MongoDB where I create, save and then update users:
public class ExampleMongoDB {
public static void main(String[] args) {
final MongoClient mongoClient = MongoClients.create();
final MongoDatabase database = mongoClient.getDatabase("db");
database.drop();
final CodecRegistry pojoCodecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(),
fromProviders(PojoCodecProvider.builder().automatic(true).build()));
final MongoCollection<User> users = database.getCollection("users", User.class).withCodecRegistry(pojoCodecRegistry);
users.insertMany(new ExampleMongoDB().getRandomUsers());
System.out.println("Before updating:");
users.find(new Document("role", "ADMIN")).iterator().forEachRemaining(
System.out::println
);
System.out.println("After updating:");
users.updateMany(eq("role", "ADMIN"), set("role", "GUEST"));
users.find(new Document("role", "GUEST")).iterator().forEachRemaining(
System.out::println
);
}
public List<User> getRandomUsers() {
final ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 15; i++) {
users.add(
User.builder()
.login("log" + i)
.name("name" + i)
.pass("pass" + i)
.role(
(i % 2 == 0) ? User.Role.ADMIN : User.Role.USER
).build()
);
}
return users;
}
}
This should work (it worked for me):
#Builder(builderClassName = "Builder")
#Value
#AllArgsConstructor(onConstructor = #__(#BsonCreator))
#BsonDiscriminator
public class User {
#BsonId
private final ObjectId _id;
#BsonProperty("name")
private final String name;
#BsonProperty("pass")
private final String pass;
#BsonProperty("login")
private final String login;
#BsonProperty("role")
private final Role role;
}
Then in lombok.config add these (in your module/project directory):
lombok.addLombokGeneratedAnnotation=true
lombok.anyConstructor.addConstructorProperties=true
lombok.copyableAnnotations += org.bson.codecs.pojo.annotations.BsonProperty
lombok.copyableAnnotations += org.bson.codecs.pojo.annotations.BsonId
Also piece of advice, keep _id if you are going to use automatic conversion to POJOs using PojoCodec, which will save a lot of trouble.

Jackson: deserialize with Builder along with standard setters/getters?

Is it possible with Jackson to deserialize json with Builder pattern as well as with default setter and getter approach?
My object is created with Builder that covers only required (final) fields, but I have non-final fields with some values as well that need to be deserialized with setters.
Here is the sample that throws an exception in an attempt to deserialize it with:
new ObjectMapper().readValue(json, Foo.class);
json - json representation serialized with default Jackson serializer, like:
objectMapper.writeValueAsString(foo);
class
#Getter
#Setter
#ToString
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonDeserialize(builder = Foo.Builder.class)
public class Foo {
private final String key;
private final Long user;
private final String action;
private final String material;
private final String currency;
private Foo(String key, Long user, String action, String material, String currency) {
this.key = key;
this.user = user;
this.action = action;
this.material = material;
this.currency = currency;
}
public static class Builder {
private String key;
private Long user;
private String action;
private String material;
private String currency;
#JsonProperty("key")
public Foo.Builder withKey(String key) {
this.key = key;
return this;
}
#JsonProperty("user")
public Foo.Builder withUser(Long user) {
this.user = user;
return this;
}
#JsonProperty("action")
public Foo.Builder withAction(String action) {
this.action = action;
return this;
}
/// other 'with' setters....
}
#JsonProperty("state")
private int state;
#JsonProperty("stat")
private String stat;
#JsonProperty("step")
private String step;
}
The exception it throws like :
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:
Unrecognized field "state" (class com.Foo$Builder), not marked as
ignorable (5 known properties: "key", "user", "action", "material",
"currency",])
If not possible what workaround is the cheapest?
Two things that are suspicious:
You are willing to use the builder inside the Foo class. In that case you should correct the specification
(SessionData.Builder.class is not correct in that case).
You are indeed trying to use an external builder. In this case you should remove or at least mark as ignorable the inner builder, this seems to be the reason of the excetpion you are getting.
In both cases you should make sure the final method to get the Foo instance is called build() otherwise you should annotate the builder with a #JsonPOJOBuilder(buildMethodName = "nameOfMethod", withPrefix = "set").

Ignore fields from Java object dynamically while sending as JSON from Spring MVC

I have model class like this, for hibernate
#Entity
#Table(name = "user", catalog = "userdb")
#JsonIgnoreProperties(ignoreUnknown = true)
public class User implements java.io.Serializable {
private Integer userId;
private String userName;
private String emailId;
private String encryptedPwd;
private String createdBy;
private String updatedBy;
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "UserId", unique = true, nullable = false)
public Integer getUserId() {
return this.userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
#Column(name = "UserName", length = 100)
public String getUserName() {
return this.userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
#Column(name = "EmailId", nullable = false, length = 45)
public String getEmailId() {
return this.emailId;
}
public void setEmailId(String emailId) {
this.emailId = emailId;
}
#Column(name = "EncryptedPwd", length = 100)
public String getEncryptedPwd() {
return this.encryptedPwd;
}
public void setEncryptedPwd(String encryptedPwd) {
this.encryptedPwd = encryptedPwd;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
#Column(name = "UpdatedBy", length = 100)
public String getUpdatedBy() {
return this.updatedBy;
}
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}
}
In Spring MVC controller, using DAO, I am able to get the object. and returning as JSON Object.
#Controller
public class UserController {
#Autowired
private UserService userService;
#RequestMapping(value = "/getUser/{userId}", method = RequestMethod.GET)
#ResponseBody
public User getUser(#PathVariable Integer userId) throws Exception {
User user = userService.get(userId);
user.setCreatedBy(null);
user.setUpdatedBy(null);
return user;
}
}
View part is done using AngularJS, so it will get JSON like this
{
"userId" :2,
"userName" : "john",
"emailId" : "john#gmail.com",
"encryptedPwd" : "Co7Fwd1fXYk=",
"createdBy" : null,
"updatedBy" : null
}
If I don't want to set encrypted Password, I will set that field also as null.
But I don't want like this, I dont want to send all fields to client side. If I dont want password, updatedby, createdby fields to send, My result JSON should be like
{
"userId" :2,
"userName" : "john",
"emailId" : "john#gmail.com"
}
The list of fields which I don't want to send to client coming from other database table. So it will change based on the user who is logged in. How can I do that?
I hope You got my question.
Add the #JsonIgnoreProperties("fieldname") annotation to your POJO.
Or you can use #JsonIgnore before the name of the field you want to ignore while deserializing JSON. Example:
#JsonIgnore
#JsonProperty(value = "user_password")
public String getUserPassword() {
return userPassword;
}
GitHub example
Can I do it dynamically?
Create view class:
public class View {
static class Public { }
static class ExtendedPublic extends Public { }
static class Internal extends ExtendedPublic { }
}
Annotate you model
#Document
public class User {
#Id
#JsonView(View.Public.class)
private String id;
#JsonView(View.Internal.class)
private String email;
#JsonView(View.Public.class)
private String name;
#JsonView(View.Public.class)
private Instant createdAt = Instant.now();
// getters/setters
}
Specify the view class in your controller
#RequestMapping("/user/{email}")
public class UserController {
private final UserRepository userRepository;
#Autowired
UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
#RequestMapping(method = RequestMethod.GET)
#JsonView(View.Internal.class)
public #ResponseBody Optional<User> get(#PathVariable String email) {
return userRepository.findByEmail(email);
}
}
Data example:
{"id":"5aa2496df863482dc4da2067","name":"test","createdAt":"2018-03-10T09:35:31.050353800Z"}
UPD: keep in mind that it's not best practice to use entity in response. Better use different DTO for each case and fill them using modelmapper
I know I'm a bit late to the party, but I actually ran into this as well a few months back. All of the available solutions weren't very appealing to me (mixins? ugh!), so I ended up creating a new library to make this process cleaner. It's available here if anyone would like to try it out: https://github.com/monitorjbl/spring-json-view.
The basic usage is pretty simple, you use the JsonView object in your controller methods like so:
import com.monitorjbl.json.JsonView;
import static com.monitorjbl.json.Match.match;
#RequestMapping(method = RequestMethod.GET, value = "/myObject")
#ResponseBody
public void getMyObjects() {
//get a list of the objects
List<MyObject> list = myObjectService.list();
//exclude expensive field
JsonView.with(list).onClass(MyObject.class, match().exclude("contains"));
}
You can also use it outside of Spring:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import static com.monitorjbl.json.Match.match;
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(JsonView.class, new JsonViewSerializer());
mapper.registerModule(module);
mapper.writeValueAsString(JsonView.with(list)
.onClass(MyObject.class, match()
.exclude("contains"))
.onClass(MySmallObject.class, match()
.exclude("id"));
Yes, you can specify which fields are serialized as JSON response and which to ignore.
This is what you need to do to implement Dynamically ignore properties.
1) First, you need to add #JsonFilter from com.fasterxml.jackson.annotation.JsonFilter on your entity class as.
import com.fasterxml.jackson.annotation.JsonFilter;
#JsonFilter("SomeBeanFilter")
public class SomeBean {
private String field1;
private String field2;
private String field3;
// getters/setters
}
2) Then in your controller, you have to add create the MappingJacksonValue object and set filters on it and in the end, you have to return this object.
import java.util.Arrays;
import java.util.List;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
#RestController
public class FilteringController {
// Here i want to ignore all properties except field1,field2.
#GetMapping("/ignoreProperties")
public MappingJacksonValue retrieveSomeBean() {
SomeBean someBean = new SomeBean("value1", "value2", "value3");
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("field1", "field2");
FilterProvider filters = new SimpleFilterProvider().addFilter("SomeBeanFilter", filter);
MappingJacksonValue mapping = new MappingJacksonValue(someBean);
mapping.setFilters(filters);
return mapping;
}
}
This is what you will get in response:
{
field1:"value1",
field2:"value2"
}
instead of this:
{
field1:"value1",
field2:"value2",
field3:"value3"
}
Here you can see it ignores other properties(field3 in this case) in response except for property field1 and field2.
Hope this helps.
We can do this by setting access to JsonProperty.Access.WRITE_ONLY while declaring the property.
#JsonProperty( value = "password", access = JsonProperty.Access.WRITE_ONLY)
#SerializedName("password")
private String password;
Add #JsonInclude(JsonInclude.Include.NON_NULL) (forces Jackson to serialize null values) to the class as well as #JsonIgnore to the password field.
You could of course set #JsonIgnore on createdBy and updatedBy as well if you always want to ignore then and not just in this specific case.
UPDATE
In the event that you do not want to add the annotation to the POJO itself, a great option is Jackson's Mixin Annotations. Check out the documentation
I've solved using only #JsonIgnore like #kryger has suggested.
So your getter will become:
#JsonIgnore
public String getEncryptedPwd() {
return this.encryptedPwd;
}
You can set #JsonIgnore of course on field, setter or getter like described here.
And, if you want to protect encrypted password only on serialization side (e.g. when you need to login your users), add this #JsonProperty annotation to your field:
#JsonProperty(access = Access.WRITE_ONLY)
private String encryptedPwd;
More info here.
If I were you and wanted to do so, I wouldn't use my User entity in Controller layer.Instead I create and use UserDto (Data transfer object) to communicate with business(Service) layer and Controller.
You can use Apache BeanUtils(copyProperties method) to copy data from User entity to UserDto.
I have created a JsonUtil which can be used to ignore fields at runtime while giving a response.
Example Usage :
First argument should be any POJO class (Student) and ignoreFields is comma seperated fields you want to ignore in response.
Student st = new Student();
createJsonIgnoreFields(st,"firstname,age");
import java.util.logging.Logger;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;
import org.codehaus.jackson.map.ser.FilterProvider;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
public class JsonUtil {
public static String createJsonIgnoreFields(Object object, String ignoreFields) {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.getSerializationConfig().addMixInAnnotations(Object.class, JsonPropertyFilterMixIn.class);
String[] ignoreFieldsArray = ignoreFields.split(",");
FilterProvider filters = new SimpleFilterProvider()
.addFilter("filter properties by field names",
SimpleBeanPropertyFilter.serializeAllExcept(ignoreFieldsArray));
ObjectWriter writer = mapper.writer().withFilters(filters);
return writer.writeValueAsString(object);
} catch (Exception e) {
//handle exception here
}
return "";
}
public static String createJson(Object object) {
try {
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withDefaultPrettyPrinter();
return writer.writeValueAsString(object);
}catch (Exception e) {
//handle exception here
}
return "";
}
}
I've found a solution for me with Spring and jackson
First specify the filter name in the entity
#Entity
#Table(name = "SECTEUR")
#JsonFilter(ModelJsonFilters.SECTEUR_FILTER)
public class Secteur implements Serializable {
/** Serial UID */
private static final long serialVersionUID = 5697181222899184767L;
/**
* Unique ID
*/
#Id
#JsonView(View.SecteurWithoutChildrens.class)
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#JsonView(View.SecteurWithoutChildrens.class)
#Column(name = "code", nullable = false, length = 35)
private String code;
/**
* Identifiant du secteur parent
*/
#JsonView(View.SecteurWithoutChildrens.class)
#Column(name = "id_parent")
private Long idParent;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "id_parent")
private List<Secteur> secteursEnfants = new ArrayList<>(0);
}
Then you can see the constants filters names class with the default FilterProvider used in spring configuration
public class ModelJsonFilters {
public final static String SECTEUR_FILTER = "SecteurFilter";
public final static String APPLICATION_FILTER = "ApplicationFilter";
public final static String SERVICE_FILTER = "ServiceFilter";
public final static String UTILISATEUR_FILTER = "UtilisateurFilter";
public static SimpleFilterProvider getDefaultFilters() {
SimpleBeanPropertyFilter theFilter = SimpleBeanPropertyFilter.serializeAll();
return new SimpleFilterProvider().setDefaultFilter(theFilter);
}
}
Spring configuration :
#EnableWebMvc
#Configuration
#ComponentScan(basePackages = "fr.sodebo")
public class ApiRootConfiguration extends WebMvcConfigurerAdapter {
#Autowired
private EntityManagerFactory entityManagerFactory;
/**
* config qui permet d'éviter les "Lazy loading Error" au moment de la
* conversion json par jackson pour les retours des services REST<br>
* on permet à jackson d'acceder à sessionFactory pour charger ce dont il a
* besoin
*/
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
// config d'hibernate pour la conversion json
mapper.registerModule(getConfiguredHibernateModule());//
// inscrit les filtres json
subscribeFiltersInMapper(mapper);
// config du comportement de json views
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
converter.setObjectMapper(mapper);
converters.add(converter);
}
/**
* config d'hibernate pour la conversion json
*
* #return Hibernate5Module
*/
private Hibernate5Module getConfiguredHibernateModule() {
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
Hibernate5Module module = new Hibernate5Module(sessionFactory);
module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
return module;
}
/**
* inscrit les filtres json
*
* #param mapper
*/
private void subscribeFiltersInMapper(ObjectMapper mapper) {
mapper.setFilterProvider(ModelJsonFilters.getDefaultFilters());
}
}
Endly I can specify a specific filter in restConstoller when i need....
#RequestMapping(value = "/{id}/droits/", method = RequestMethod.GET)
public MappingJacksonValue getListDroits(#PathVariable long id) {
LOGGER.debug("Get all droits of user with id {}", id);
List<Droit> droits = utilisateurService.findDroitsDeUtilisateur(id);
MappingJacksonValue value;
UtilisateurWithSecteurs utilisateurWithSecteurs = droitsUtilisateur.fillLists(droits).get(id);
value = new MappingJacksonValue(utilisateurWithSecteurs);
FilterProvider filters = ModelJsonFilters.getDefaultFilters().addFilter(ModelJsonFilters.SECTEUR_FILTER, SimpleBeanPropertyFilter.serializeAllExcept("secteursEnfants")).addFilter(ModelJsonFilters.APPLICATION_FILTER,
SimpleBeanPropertyFilter.serializeAllExcept("services"));
value.setFilters(filters);
return value;
}
Place #JsonIgnore on the field or its getter, or create a custom dto
#JsonIgnore
private String encryptedPwd;
or as mentioned above by ceekay annotate it with #JsonProperty where access attribute is set to write only
#JsonProperty( value = "password", access = JsonProperty.Access.WRITE_ONLY)
private String encryptedPwd;
Can I do it dynamically?
Yes, you can use a combination of Jackson's PropertyFilter and mixins.
Explanation
Jackson has a PropertyFilter interface to implement a filter to ignore fields dynamically. The problem is that filter has to be defined on the DTO/POJO class using the #JsonFilter annotation.
To avoid adding a #JsonFilter on class we can use ObjectMapper's addMixIn method to "dynamically" add this annotation (and leave our DTO/POJO classes as is).
Code example
Here is my implementation of the idea provided above. We can call toJson() with two arguments: (1) object to be serialized and (2) lambda (Java's Predicate) to be used in PropertyFilter:
public class JsonService {
public String toJson(Object object, Predicate<PropertyWriter> filter) {
ObjectMapper mapper = new ObjectMapper();
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("DynamicFilter", new DynamicFilter(filter));
mapper.setFilterProvider(filterProvider);
mapper.addMixIn(object.getClass(), DynamicFilterMixin.class);
try {
return mapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new MyException(e);
}
}
private static final class DynamicFilter extends SimpleBeanPropertyFilter {
private Predicate<PropertyWriter> filter;
private DynamicFilter(Predicate<PropertyWriter> filter) {
this.filter = filter;
}
protected boolean include(BeanPropertyWriter writer) {
return include((PropertyWriter) writer);
}
protected boolean include(PropertyWriter writer) {
return filter.test(writer);
}
}
#JsonFilter("DynamicFilter")
private interface DynamicFilterMixin {
}
}
Now we can call toJson and filter fields during a serialization:
Filtering by name
new JsonService().toJson(object, w -> !w.getName().equals("fieldNameToBeIgnored"));
Filtering by annotation (on the field)
new JsonService().toJson(object, w -> w.getAnnotation(MyAnnotation.class) == null);
Unit tests
Here are the unit tests for the class above:
public class JsonServiceTest {
private JsonService jsonService = new JsonService();
#Test
public void withoutFiltering() {
MyObject object = getObject();
String json = jsonService.toJson(object, w -> true);
assertEquals("{\"myString\":\"stringValue\",\"myInteger\":10,\"myBoolean\":true}", json);
}
#Test
public void filteredByFieldName() {
MyObject object = getObject();
String json = jsonService.toJson(object, w -> !w.getName().equals("myString"));
assertEquals("{\"myInteger\":10,\"myBoolean\":true}", json);
}
#Test
public void filteredByAnnotation() {
MyObject object = getObject();
String json = jsonService.toJson(object, w -> w.getAnnotation(Deprecated.class) == null);
assertEquals("{\"myString\":\"stringValue\",\"myInteger\":10}", json);
}
private MyObject getObject() {
MyObject object = new MyObject();
object.myString = "stringValue";
object.myInteger = 10;
object.myBoolean = true;
return object;
}
private static class MyObject {
private String myString;
private int myInteger;
#Deprecated
private boolean myBoolean;
public String getMyString() {
return myString;
}
public void setMyString(String myString) {
this.myString = myString;
}
public int getMyInteger() {
return myInteger;
}
public void setMyInteger(int myInteger) {
this.myInteger = myInteger;
}
public boolean isMyBoolean() {
return myBoolean;
}
public void setMyBoolean(boolean myBoolean) {
this.myBoolean = myBoolean;
}
}
}
Would not creating a UserJsonResponse class and populating with the wanted fields be a cleaner solution?
Returning directly a JSON seems a great solution when you want to give all the model back. Otherwise it just gets messy.
In the future, for example you might want to have a JSON field that does not match any Model field and then you're in a bigger trouble.
This is a clean utility tool for the above answer :
#GetMapping(value = "/my-url")
public #ResponseBody
MappingJacksonValue getMyBean() {
List<MyBean> myBeans = Service.findAll();
MappingJacksonValue mappingValue = MappingFilterUtils.applyFilter(myBeans, MappingFilterUtils.JsonFilterMode.EXCLUDE_FIELD_MODE, "MyFilterName", "myBiggerObject.mySmallerObject.mySmallestObject");
return mappingValue;
}
//AND THE UTILITY CLASS
public class MappingFilterUtils {
public enum JsonFilterMode {
INCLUDE_FIELD_MODE, EXCLUDE_FIELD_MODE
}
public static MappingJacksonValue applyFilter(Object object, final JsonFilterMode mode, final String filterName, final String... fields) {
if (fields == null || fields.length == 0) {
throw new IllegalArgumentException("You should pass at least one field");
}
return applyFilter(object, mode, filterName, new HashSet<>(Arrays.asList(fields)));
}
public static MappingJacksonValue applyFilter(Object object, final JsonFilterMode mode, final String filterName, final Set<String> fields) {
if (fields == null || fields.isEmpty()) {
throw new IllegalArgumentException("You should pass at least one field");
}
SimpleBeanPropertyFilter filter = null;
switch (mode) {
case EXCLUDE_FIELD_MODE:
filter = SimpleBeanPropertyFilter.serializeAllExcept(fields);
break;
case INCLUDE_FIELD_MODE:
filter = SimpleBeanPropertyFilter.filterOutAllExcept(fields);
break;
}
FilterProvider filters = new SimpleFilterProvider().addFilter(filterName, filter);
MappingJacksonValue mapping = new MappingJacksonValue(object);
mapping.setFilters(filters);
return mapping;
}
}
To acheive dynamic filtering follow the link - https://iamvickyav.medium.com/spring-boot-dynamically-ignore-fields-while-converting-java-object-to-json-e8d642088f55
Add the #JsonFilter("Filter name") annotation to the model class.
Inside the controller function add the code:-
SimpleBeanPropertyFilter simpleBeanPropertyFilter =
SimpleBeanPropertyFilter.serializeAllExcept("id", "dob");
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("Filter name", simpleBeanPropertyFilter);
List<User> userList = userService.getAllUsers();
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(userList);
mappingJacksonValue.setFilters(filterProvider);
return mappingJacksonValue;
make sure the return type is MappingJacksonValue.
Hi I have achieved dynamic filtering by using Gson library like in the below:
JsonObject jsonObj = new Gson().fromJson(mapper.writeValueAsString(sampleObject), JsonObject.class);
jsonObj.remove("someProperty");
String data = new Gson().toJson(jsonObj);
In your entity class add #JsonInclude(JsonInclude.Include.NON_NULL) annotation to resolve the problem
it will look like
#Entity
#JsonInclude(JsonInclude.Include.NON_NULL)

Categories