Luiz Fernando Noschang
Luiz Fernando Noschang

Reputation: 529

Spring: can't persist entity annotated with @JsonIgnore

I'm a beginner in Spring Boot and can't manage to solve a problem. I have an entity class (Customer) and a REST repository (CustomerRepository). The class contains some sensitive fields that I don't want to be exposed by the REST repository. So, I annotated these fields with the @JsonIgnore annotation as follows:

package br.univali.sapi.entities;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Customer
{
    @Id
    @GeneratedValue
    private Long id = null;

    private String login;

    private String name;

    @JsonIgnore
    private String password;

    @JsonIgnore
    private String email;

    public Customer()
    {

    }

    public Long getId()
    {
            return id;
    }

    public void setId(Long id)
    {
            this.id = id;
    }

    public String getLogin()
    {
            return login;
    }

    public void setLogin(String login)
    {
            this.login = login;
    }

    public String getName()
    {
            return name;
    }

    public void setName(String name)
    {
            this.name = name;
    }

    public String getPassword()
    {
            return password;
    }

    public void setPassword(String password)
    {
            this.password = password;
    }

    public String getEmail()
    {
            return email;
    }

    public void setEmail(String email)
    {
            this.email = email;
    }
}

Everything worked fine and my REST API returned the desired results. However, when I do a POST request to the API in order to insert a new entity I receive database errors: "column password can't be null", "column email can't be null".

The password and email are being sent to the server in the POST request along with the other parameters, however it seems to be ignored. If I remove the @JsonIgnore annotation the entity is persisted normally.

I know I could use a projection to hide these fields. But the projection is an optional parameter in the URL. This way an experienced user would be able to remove the parameter from the request URL and see these fields anyway.

If I could implicitly enforce a projection, that would solve the problem, but this seems to be impossible to do using only the Spring framework. I could achieve this using an Apache URL rewrite but the maintenance would suck.

Anyone has an idea on how I can solve this? Thanks!

EDIT 1:

I believe I found a solution using DTOs/projections (Data Transfer Objects). You have to create two DTOs, one for displaying the entity and another for updating the entity, as follows:

public interface CustomerViewDTO
{
    public Long getId();
    public String getLogin();
    public String getName();
}
public class CustomerUpdateDTO
{
   private String login;
   private String name;
   private String password;
   private String email;

   // Getters and Setters ommited for breviety
}

Now, on the repository you use the DTO and Spring will do it's magic:

@Transactional(readOnly = true)
public interface CustomerRepository extends JPARepository<Customer, Long>
{
   // Using derived query
   public CustomerViewDTO findByIdAsDTO(Long id);

   // Using @Query annotation
   @Query("SELECT C FROM Customer C WHERE C.id = :customerId")
   public CustomerViewDTO findByIdAsDTO(@Param("customerId") Long id);
}   

And for updating, you receive the DTO on your controller and map it to the entity on your service, like this:

@RestController
public class CustomerController
{
    @Autowired
    private CustomerService customerService;

    @RequestMapping(method = "PATCH", path = "/customers/{customerId}")
    public ResponseEntity<?> updateCustomer(@PathVariable Long customerId, @RequestBody CustomerUpdateDTO customerDTO)
    {
        CustomerViewDTO updatedCustomer = customerService.updateCustomer(customerId, customerDTO);

        return ResponseEntity.ok(updatedCustomer);
    }

    @RequestMapping(method = GET, path = "/customers/{customerId}")
    public ResponseEntity<?> findCustomerById(@PathVariable Long customerId)
    {
        return ResponseEntity.ok(customerService.findByIdAsDTO(Long));
    }
}
@Service
public class CustomerService
{
    @Autowired
    private CustomerRepository customerRepository;

    // Follow this tutorial:
    // https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application
    @Autowired
    private ModelMapper modelMapper; 

    @Transactional(readOnly = false)
    public CustomerViewDTO updateCustomer(Long customerId, CustomerUpdateDTO customerDTO)
    {
         Customer customerEntity = customerRepository.findById(customerId);

         // This copies all values from the DTO to the entity
         modelMapper.map(customerDTO, customerEntity); 

         // Now we have two aproaches: 
         // 1. save the entity and convert back to DTO manually using the model mapper
         // 2. save the entity and call the repository method which will convert to the DTO automatically
         // The second approach is the one I use for several reasons that
         // I won't explain here

         // Here we use save and flush to force JPA to execute the update query. This way, when we do the select the entity will come with the fields updated
         customerEntity = customerRepository.saveAndFlush(customerEntity);

         // First aproach
         return modelMapper.map(customerEntity, CustomerViewDTO.class);

         // Second approach
         return customerRepository.findByIdAsDTO(customerId);
    }

    @Transactional(readOnly = true)
    public CustomerViewDTO findByIdAsDTO(Long customerId)
    {
         return customerRepository.findByIdAsDTO(customerId);
    }
}

Upvotes: 7

Views: 3640

Answers (3)

Using Jackson annotation would be better to use it in your Dto or Resources classes instead of using it in Entity classes. so you can easly create your own request and response DTO/Resources. So you can use Jackson annotations as you want by creating your DTO or Resources classes. If you only use it in your Entity classes, you will have a hard time performing the operations you want. This is because your Entity classes are database related, so they should only talk to the database. It is not correct to perform requests or responses from the client. You already answered the question at the very beginning. For example, let's say you used the JsonIgnoreProperties annotation on any field in your Entity class. For example You don't want the value of any variable to appear when you return Customers in your Controller layer. But your Customer entity class also sends it as POST request and there is a problem. Therefore, instead of doing these operations on entity classes, you should wrap the responses you expect or the requests you receive in DTO/Resources classes.

Upvotes: 0

Partho
Partho

Reputation: 2725

There are many ways to do this

Using this

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;

instead of this

@JsonIgnore
private String password;

Another way

  • @JsonIgnore
  • @JsonProperty

Use @JsonIgnore on the class member and its getter, and @JsonProperty on its setter.

package br.univali.sapi.entities;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Customer
{
@Id
@GeneratedValue
private Long id = null;

private String login;

private String name;

@JsonIgnore
private String password;

@JsonIgnore
private String email;

public Customer()
{

}
@JsonIgnore
public String getPassword()
{
        return password;
}

@JsonProperty
public void setPassword(String password)
{
        this.password = password;
}

@JsonIgnore
public String getEmail()
{
        return email;
}

@JsonProperty
public void setEmail(String email)
{
        this.email = email;
 }
}

Upvotes: 0

yyunikov
yyunikov

Reputation: 5907

I suppose you have not null constraint on db level. Basically when you do @JsonIgnore on field you just not passing it and database can't insert that value, that's pretty clear.

So the solution I see here is the following:

1) Remove @JsonIgnore annotations to be able to do POST requests.

2) Use projections when you use GET. You can configure Spring Data REST to always use projections, however by default Spring Data REST always return full JSON on single resource (in you case with email and password) which is pretty weird for me. Solution to this problem I've wrote in another answer as I also had the same problem. Just use ProjectingProcessor and use some default name for all projections. Don't forget to add projection in your configuration using config.getProjectionConfiguration().addProjection method.

3) The only way to avoid step 2 is to forbid GET on single resource on Spring Data REST level and use custom controllers. But I would go with the solution in step 2 to avoid boilerplate controllers.

Upvotes: 0

Related Questions