I have a simple web controller to return an User entity.
This entity has a property nonEditableProperty that cannot be updated.
It's works fine on the web controller, the nonEditableProperty value is listed but on the UserControllerTest it doesn't work and the returned value is always null.
The annotation #JsonProperty(access = JsonProperty.Access.READ_ONLY) seems to be ignored during the test serialization.
Does anyone have any clue for this issue?
Should I load some Jackson configuration for the tests?
#Getter
#Entity
class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String nonEditableProperty;
User() {
}
public User(String name, String nonEditableProperty) {
this.name = name;
this.nonEditableProperty = nonEditableProperty;
}
}
#RestController
#AllArgsConstructor
#RequestMapping("users")
public class UserController {
private final UserRepository userRepository;
#GetMapping
public Collection<User> getAllUsers() {
return (Collection<User>) userRepository.findAll();
}
#GetMapping(path = "/{id}")
public User getUser(#PathVariable Integer id) {
return userRepository.findOne(id);
}
}
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest {
#Autowired
TestRestTemplate testRestTemplate;
#Test
public void getUserShouldReturnData() {
ResponseEntity<User> response = testRestTemplate.getForEntity("/users/{id}", User.class, 1);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getName()).isEqualTo("Muhammed SuiƧmez");
assertThat(response.getBody().getNonEditableProperty()).isEqualTo("Non editable property");
}
}
testRestTemplate.getForEntity(URI url, Class<T> responseType) Fetches the api response by hitting url and then converts response to the type given by responseType
Though the API response fetched by hitting URL received the nonEditableProperty value (parse inputMessage.getBody here), While deserializing it to responseType the value was lost, because of READ_ONLY Jackson property.
Related
Rest Controller:
#RequestMapping(value = "/admin/rest/new-subscriptions")
public List<NewSubscriptionDTO> getNewSubscriptions() {
NewSubscriptionDTO dto = new NewSubscriptionDTO();
dto.setId("54");
dto.setName("John Doe");
return Arrays.asList(dto);
}
NewSubscriptionDTO:
package dermatica.web.admin.rx;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.joda.time.DateTime;
import java.io.Serializable;
public class NewSubscriptionDTO implements Serializable {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
I get the following exception:
no properties discovered to create BeanSerializer (to avoid exception,
disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
If I annotate the fields with #JsonProperty it work fine.
Is there a way for the serialization to work automatically without needing this annotation?
#JsonProperty auto-generates a getter/setter that Jackson uses to read/write to the fields during serialization/deserialization. Here are some alternative approaches:
Provide your own public getters/setters for all fields
Make the fields public, generally frowned upon, but if you're creating a simple DTO, that may be acceptable.
Setting ObjectMapper Visibility for FIELD to ANY (see here)
Disable the FAIL_ON_EMPTY_BEANS exception (see here)
Given that your DTO class has getters and setters, this should work without #JsonProperty. I wasn't able to reproduce the exact error message you showed, but here are some suggestions that may help:
[Controller] Explicitly specify the method type as GET, either using method = GET or #GetMapping - not necessary, but it's good to be explicit
[Controller] Make sure you annotate the controller class with #RestController, indicating the response is serialized to JSON and wrapped in an HttpResponse object.
[DTO] You don't need to extend Serializable (see here).
The final controller would look like this:
#RestController
public class MyController {
#GetMapping(value = "/admin/rest/new-subscriptions")
public List<MyDTO> getDTO() {
MyDTO dto = new MyDTO();
dto.setId("54");
dto.setName("John Doe");
return Collections.singletonList(dto);
}
}
Response:
[{"id":"54","name":"John Doe"}]
I got the following test entity:
public class Test {
public String id;
public String name;
}
My test resource looks like this:
#Path("test")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class TestResource {
#Path("{id}")
#POST
public Test test(#Valid Test test){
return test;
}
}
If I do a POST with name as request body, I will end up with an entity which has a name but no id set. If I want to have set the id, I define a #PathParam("id") String id and then set the id with test.id = id. That is what I am using right now.
In this case, if I put a #NotNull constraint to the ID, the validation fails.
How can I 'put' the parsed ID to the request body, before the validation is happening? Ideal, not manually in any case.
You should remove the #NotNull annotation and ensure that no ID is given from the POST request. And in your GET endpoints ensure that the field is not null, probably with an own annotation. I did this with my own annotation #TestIdNull(false).
In your code you would place the annotation like this:
#Path("{id}")
#POST
public Test test(#TestIdNull(true) Test test){
return test;
}
while the annotation interface looks quite simple:
#Target({ElementType.PARAMETER,ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = PetIdNullValidator.class)
public #interface TestIdNull {
String message() default "Post request. ID value forbidden.";
boolean value() default true;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and the implementation would be
#Provider
#NoArgsConstructor
public class TestIdNullValidator implements ConstraintValidator<TestIdNull, Test> {
private boolean switchNullCheck;
/**
* {#inheritDoc}
*/
#Override
public void initialize(final TestIdNull constraintAnnotation) {
switchNullCheck = constraintAnnotation.value();
}
/**
* {#inheritDoc}
*/
#Override
public boolean isValid(final Test test, final ConstraintValidatorContext context) {
if (null == test) {
return true;
}
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("ENTER YOUR MESSAGE HERE").addConstraintViolation();
return switchNullCheck ? test.id() == null : test.id() != null;
}
}
On your "way back" (the GET methods) you must ensure the ID is not null with placing the annotation to your DTO. Code could look like this:
#TestIdNull(false)
final Test test = mapper.map(entity, Test.class);
I know it's not a very elegant way but your idea placing the ID in a POST first and then validate is not possible I'm afraid.
I am trying to restrict which properties from a JSON object are deserialised using Jackson JSONViews. The aim is to use this to prevent consumers of my API from submitting data that they shouldn't.
The problem is, I have either misunderstood JSONViews or I am doing something wrong. See below.
I started trying to do this in Spring but have noticed that even the simple test below doesn't work.
Account Class
public class Account {
#Id
private String id;
private String name;
private List<String> items;
private List<User> users;
#JsonView(AccountViews.Private.class)
public void setId(String id) {
this.id = id;
}
#JsonView(AccountViews.Public.class)
public void setName(String name) {
this.name = name;
}
#JsonView(AccountViews.Public.class)
public void setItems(List<String> items) {
this.items = items;
}
#JsonView(AccountViews.Private.class)
public void setUsers(List<User> users) {
this.users = users;
}
}
Views
public class AccountViews {
public interface Public {}
public interface Private extends Public {}
}
Test
#Test
public void testDeserialization(){
ObjectMapper mapper = new ObjectMapper();
mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
Account account = mapper.readerWithView(AccountViews.Public.class).forType(Account.class).readValue("{ \"name\": \"account1\", \"items\": [\"item1\"], \"users\": [ { \"firstname\": \"user1_firstname\", \"lastname\": \"user1_lastname\" }] }");
assertEquals(account.getName(), "account1");
assertNull(account.getUsers());
}
Unforunately, the 2nd assertion fails because Users has a user object inside.
Basically, even though "users" is a property of Account, I don't want the value to be deserialized because I have used the JSONView (AccountViews.Public.class). However, whatever I try it always seems to be deserialized and is present on the account object.
Any help much appreciated.
Error
`java.lang.AssertionError: expected null, but was:<[User#609db43b]>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotNull(Assert.java:755)
at org.junit.Assert.assertNull(Assert.java:737)
at org.junit.Assert.assertNull(Assert.java:747)
at`
I'm working on a sample project using :
JAX-RS (Jersey 2)
JSR-303 Bean validaiton
Jersey Test for testing
Grizzly Http container
Jackson 2.7.3
Before adding the #JsonIgnore and #JsonProperty everything was working as expected, i was able to run my tests without a problem while performing bean validation on my object properties.
Normally The "password" field should not be available for deserialization so i marked its getter with #JsonIgnore and its setter with #JsonProperty so as to allow serializing it when saving a new account object.
When launching my test i receive a 400 code error with following http response result :
avr. 26, 2016 12:25:29 PM org.glassfish.jersey.filter.LoggingFilter log
INFO: 1 * Client response received on thread main
1 < 400
1 < Connection: close
1 < Content-Length: 172
1 < Content-Type: application/json
1 < Date: Tue, 26 Apr 2016 11:25:29 GMT
1 < Vary: Accept
[{"message":"may not be null","messageTemplate":"{javax.validation.constraints.NotNull.message}","path":"AccountEndpoint.saveAccount.account.password","invalidValue":null}]
Note : When removing the #JsonIgnore annotation, no validation error
is received of course
My Resource :
#Path("/account")
public class AccountEndpoint {
#POST
#Path("/save/")
#Produces(MediaType.APPLICATION_JSON)
public Response saveAccount(#Valid Account account){
Account returned = accountService.saveAccount(account);
return Response.status(200).entity(returned).build();
}
My Pojo Class :
public class Account implements Serializable {
private String username;
private String password;
// -- [username] ------------------------
#Size(max = 45)
#NotNull
#Column(name = "username", length = 45,unique=true)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
#NotNull
#Size(max = 45)
#Column(name = "password", length = 45)
#JsonIgnore
public String getPassword() {
return password;
}
#JsonProperty
public void setPassword(String password) {
this.password = password;
}
}
My Unit Test :
public class AccountTest extends JerseyTest {
#Before
public void setUp() throws Exception {
super.setUp();
}
#After
public void after() throws Exception {
super.tearDown();
}
#Override
protected TestContainerFactory getTestContainerFactory() {
return new GrizzlyWebTestContainerFactory();
}
#Override
protected DeploymentContext configureDeployment() {
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
return ServletDeploymentContext.forServlet(new ServletContainer(new
JerseyConfig()))
.contextParam("contextConfigLocation",TEST_APP_CONTEXT)
.addListener(org.springframework.web.context.ContextLoaderListener.class)
.servletPath("/").build();
}
#Test
public void testSave_New_User_Account {
Account account = new Account();
account.setUsername("Test");
account.setPassword("Test");
Response response = target("/account/save").request().
post(Entity.entity(account,MediaType.APPLICATION_JSON));
assertEquals(200, response.getStatus());
}
}
JeseyConfig Class :
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
packages("org.medel.endpoint");
}
}
It's because on the client side the object is also being serialized by Jackson. And because the password property is ignored on reads, it never get serialized. So the request is being sent with no password. You might just want to create another test class for the entity, with no annotations, just for the client testing.
Edit
So just for clarity, the problem is with the client, not the server. When the client tries to serialize the Account, it doesn't serialize the password property, as it's ignored.
You can test it, but sending the JSON as a string, that way Jackson doesn't get involved. You will see that it works fine
.post(Entity.json("{\"username\":\"user\", \"password\":\"pass\"}"));
What's the best way to hide a field on a model in Dropwizard? (If I want to be able to deserialize the field via Jackson, but hide it when serialization)
For example, if I have the following model:
class User {
private String secret;
private String username;
}
I want to be able to create a User with a secret via calling new ObjectMapper().readValue(), but I want to hide the secret field it when it's serialized into JSON.
Add #JsonIgnore annotation before the property
Or you can add #JsonIgnoreProperties annotation and specify the fields which are to be excluded
public class Foo{
#JsonIgnore
private String bar;
...
}
or
#JsonIgnoreProperties(value = { "bar" })
public class Foo {
private String bar;
...
}
or if you want to ignore this field only on serialization, and not on deserialization then
public class Foo{
private String bar;
...
#JsonIgnore
public String getBar(){
return bar;
}
public void setBar(String bar){
this.bar = bar;
}
}
If you want to hide it only during seriazliation add #JsonIgnore annotation to property getter.
class User {
#JsonProperty
private String secret;
#JsonProperty
private String username;
#JsonIgnore
public String getSecret(){
return secret;
}
public void setSecret(String secret){
this.secret = secret;
}
...
}