Spring boot @ResponseBody doesn't serialize entity id
Asked Answered
J

11

106

Have a strange problem and can't figure out how to deal with it. Have simple POJO:

@Entity
@Table(name = "persons")
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "middle_name")
    private String middleName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "comment")
    private String comment;

    @Column(name = "created")
    private Date created;

    @Column(name = "updated")
    private Date updated;

    @PrePersist
    protected void onCreate() {
        created = new Date();
    }

    @PreUpdate
    protected void onUpdate() {
        updated = new Date();
    }

    @Valid
    @OrderBy("id")
    @OneToMany(mappedBy = "person", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<PhoneNumber> phoneNumbers = new ArrayList<>();

    public Long getId() {
        return id;
    }

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

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getMiddleName() {
        return middleName;
    }

    public void setMiddleName(String middleName) {
        this.middleName = middleName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public Date getCreated() {
        return created;
    }

    public Date getUpdated() {
        return updated;
    }

    public List<PhoneNumber> getPhoneNumbers() {
        return phoneNumbers;
    }

    public void addPhoneNumber(PhoneNumber number) {
        number.setPerson(this);
        phoneNumbers.add(number);
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

@Entity
@Table(name = "phone_numbers")
public class PhoneNumber {

    public PhoneNumber() {}

    public PhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "phone_number")
    private String phoneNumber;

    @ManyToOne
    @JoinColumn(name = "person_id")
    private Person person;

    public Long getId() {
        return id;
    }

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

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

and rest endpoint:

@ResponseBody
@RequestMapping(method = RequestMethod.GET)
public List<Person> listPersons() {
    return personService.findAll();
}

In json response there are all fields except Id, which I need on front end side to edit/delete person. How can I configure spring boot to serialize Id as well?

That's how response looks like now:

[{
  "firstName": "Just",
  "middleName": "Test",
  "lastName": "Name",
  "comment": "Just a comment",
  "created": 1405774380410,
  "updated": null,
  "phoneNumbers": [{
    "phoneNumber": "74575754757"
  }, {
    "phoneNumber": "575757547"
  }, {
    "phoneNumber": "57547547547"
  }]
}]

UPD Have bidirectional hibernate mapping, maybe it's somehow related to issue.

Jernigan answered 19/7, 2014 at 11:12 Comment(3)
Could you please give us more insights about your spring setup? What json marshaller do you use? The default one, jackson, ...?Crissy
Actually there is no special setup. Wanted to try out spring boot :) Added spring-boot-starter-data-rest to pom and using @EnableAutoConfiguration that's all. Read couple of tutorials and seems like all have to work out of the box. And it is, except that Id field. Updated post with endpoint response.Jernigan
In Spring 4 you should also use @RestController on controller class and remove @ResponseBody from methods. Also I would suggest having DTO classes to handle json requests/responses instead of domain objects.Elegiac
R
148

I recently had the same problem and it's because that's how spring-boot-starter-data-rest works by default. See my SO question -> While using Spring Data Rest after migrating an app to Spring Boot, I have observed that entity properties with @Id are no longer marshalled to JSON

To customize how it behaves, you can extend RepositoryRestConfigurerAdapter to expose IDs for specific classes.

import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;

@Configuration
public class RepositoryConfig extends RepositoryRestConfigurerAdapter {
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(Person.class);
    }
}
Repletion answered 31/7, 2014 at 18:41 Comment(4)
And don't forget to support now getter and setter for the id in the entity class!.. (I forgot it and was searching much time for that)Spumescent
coudnt find RepositoryRestConfigurerAdapter in org.springframework.data.rest.webmvc.configActivate
Yields: The type RepositoryRestConfigurerAdapter is deprecated. Also does not appear to workMaunder
Indeed RepositoryRestConfigurerAdapter is deprecated in latest versions, you have to implement RepositoryRestConfigurer directly (use implements RepositoryRestConfigurer instead of extends RepositoryRestConfigurerAdapter)Vergos
R
55

In case you need to expose the identifiers for all entities:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;

import javax.persistence.EntityManager;
import javax.persistence.metamodel.Type;

@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(
                entityManager.getMetamodel().getEntities().stream()
                .map(Type::getJavaType)
                .toArray(Class[]::new));
    }
}

Note that in versions of Spring Boot prior to 2.1.0.RELEASE you must extend the (now deprecated) org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter instead of implement RepositoryRestConfigurer directly.


If you only want to expose the identifiers of entities that extends or implements specific super class or interface:

    ...
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(
                entityManager.getMetamodel().getEntities().stream()
                .map(Type::getJavaType)
                .filter(Identifiable.class::isAssignableFrom)
                .toArray(Class[]::new));
    }

If you only want to expose the identifiers of entities with a specific annotation:

    ...
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(
                entityManager.getMetamodel().getEntities().stream()
                .map(Type::getJavaType)
                .filter(c -> c.isAnnotationPresent(ExposeId.class))
                .toArray(Class[]::new));
    }

Sample annotation:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExposeId {}
Retina answered 18/3, 2018 at 6:9 Comment(5)
If one were to use the first block of code, what directory and file might it go into?Sumrall
@DavidKrider : It should be in its own file, in any directory covered by the Component Scan. Any sub package below your main app (with the @SpringBootApplication annotation) should be fine.Retina
Field entityManager in com.myapp.api.config.context.security.RepositoryRestConfig required a bean of type 'javax.persistence.EntityManager' that could not be found. Touched
I tried to add a @ConditionalOnBean(EntityManager.class) in MyRepositoryRestConfigurerAdapter , but the method is not called and the id are still not exposed. I use Spring data with spring data mybatis : github.com/hatunet/spring-data-mybatisTouched
@DimitriKopriwa : The EntityManager is part of the JPA specification and MyBatis does not implement JPA (take a look at Does MyBatis follows JPA?). So, I think you should configure all entities one by one, using the config.exposeIdsFor(...) method as in the accepted answer.Retina
S
24

Answer from @eric-peladan didn't work out of the box, but was pretty close, maybe that worked for previous versions of Spring Boot. Now this is how it is supposed to be configured instead, correct me if I'm wrong:

import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;

@Configuration
public class RepositoryConfiguration extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(User.class);
        config.exposeIdsFor(Comment.class);
    }
}
Seymourseys answered 29/2, 2016 at 15:17 Comment(1)
Confirmed that this solution works fine under Spring Boot v1.3.3.RELEASE unlike one proposed by @eric-peladan.Barvick
F
5

The class RepositoryRestConfigurerAdapter has been deprecated since 3.1, implement RepositoryRestConfigurer directly.

@Configuration
public class RepositoryConfiguration implements RepositoryRestConfigurer  {
	@Override
	public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
		config.exposeIdsFor(YouClass.class);
		RepositoryRestConfigurer.super.configureRepositoryRestConfiguration(config);
	}
}

Font: https://docs.spring.io/spring-data/rest/docs/current-SNAPSHOT/api/org/springframework/data/rest/webmvc/config/RepositoryRestConfigurer.html

Fullmouthed answered 5/9, 2019 at 3:51 Comment(0)
P
4

With Spring Boot you have to extends SpringBootRepositoryRestMvcConfiguration
if you use RepositoryRestMvcConfiguration the configuration define in application.properties may not worked

@Configuration
public class MyConfiguration extends SpringBootRepositoryRestMvcConfiguration  {

@Override
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
    config.exposeIdsFor(Project.class);
}
}

But for a temporary need You can use projection to include id in the serialization like :

@Projection(name = "allparam", types = { Person.class })
public interface ProjectionPerson {
Integer getIdPerson();
String getFirstName();
String getLastName();

}

Pickmeup answered 26/6, 2015 at 11:28 Comment(1)
In spring boot 2, this doesn't work. The class RepositoryRestMvcConfiguration doesn't have configureRepositoryRestConfiguration to override.Gingersnap
D
4

Just add @JsonProperty annotation to the Id and it works.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty
private long id;

another approach is to implement RepositoryRestConfigurerAdapter in configuration. (This approach will be usefull when you have to do marshalling in many places)

Dopester answered 7/11, 2020 at 21:5 Comment(0)
E
2
@Component
public class EntityExposingIdConfiguration extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        try {
            Field exposeIdsFor = RepositoryRestConfiguration.class.getDeclaredField("exposeIdsFor");
            exposeIdsFor.setAccessible(true);
            ReflectionUtils.setField(exposeIdsFor, config, new ListAlwaysContains());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    class ListAlwaysContains extends ArrayList {

        @Override
        public boolean contains(Object o) {
            return true;
        }
    }
}
Eyeshade answered 11/9, 2018 at 23:58 Comment(1)
Welcome to SO! When posting code in your answers, it is helpful to explain how your code solves the OP's problem :)Langelo
J
1

Hm, ok seems like I found the solution. Removing spring-boot-starter-data-rest from pom file and adding @JsonManagedReference to phoneNumbers and @JsonBackReference to person gives desired output. Json in response isn't pretty printed any more but now it has Id. Don't know what magic spring boot performs under hood with this dependency but I don't like it :)

Jernigan answered 19/7, 2014 at 15:28 Comment(3)
It's for exposing your Spring Data repositories as REST endpoints. If you don't want that feature you can leave it out. The id thing is to do with a Jackson Module that is registered by SDR I believe.Germin
RESTful API's should not expose surrogate key ID's because they mean nothing to external systems. In RESTful architecture the ID is the canonical URL for that resource.Heflin
I have read this before, but honestly I don't understand how can I link front end and back end without Id. I have to pass Id to orm for delete/update operation. Maybe you have some link, how can it be performed in true RESTful way :)Jernigan
S
1

Easy way: rename your variable private Long id; to private Long Id;

Works for me. You can read more about it here

Susceptible answered 23/11, 2016 at 8:20 Comment(2)
oh, man... that's such a code smell... really, do not do thatChord
@IgorDonin say that to Java community who loves sniffing and crawling semantics from variable/class/function names... all the time, everywhere.Livable
M
0

Implement the RepositoryRestConfigurer and use @Configuration annotation on the class.

Here's the snippet

@Configuration
public class BasicConfig implements RepositoryRestConfigurer{

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
        // TODO Auto-generated method stub
        config.exposeIdsFor(Person.class);
    }
    
}
Mongolian answered 5/10, 2021 at 18:50 Comment(0)
T
0

You can also use the static configuration method to easily enable exposing ids in a few lines.

From the Spring Data Rest RepsositoryRestConfigurer docs:

static RepositoryRestConfigurer withConfig(Consumer<RepositoryRestConfiguration> consumer)

Convenience method to easily create simple RepositoryRestConfigurer instances that solely want to tweak the RepositoryRestConfiguration.

Parameters: consumer - must not be null.

Since: 3.1

So this works for me in an existing @Configuration-annotated class:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;

@Configuration
public class ApplicationConfiguration {

    @Bean
    public RepositoryRestConfigurer repositoryRestConfigurer() {
        return RepositoryRestConfigurer.withConfig(repositoryRestConfiguration ->
            repositoryRestConfiguration.exposeIdsFor(Person.class)
        );
    }
}
Tenuis answered 8/12, 2022 at 16:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.