19

I'm using spring-boot to provide a REST interface persisted with MongoDB. I'm using the 'standard' dependencies to power it, including spring-boot-starter-data-mongodb and spring-boot-starter-web.

However, in some of my classes I have fields that I annotate @Transient so that MongoDB does not persist that information. However, this information I DO want sent out in my rest services. Unfortunately, both MongoDB and the rest controller seem to share that annotation. So when my front-end receives the JSON object, those fields are not instantiated (but still declared). Removing the annotation allows the fields to come through in the JSON object.

How I do configure what is transient for MongoDB and REST separately?

Here is my class

package com.clashalytics.domain.building;

import com.clashalytics.domain.building.constants.BuildingConstants;
import com.clashalytics.domain.building.constants.BuildingType;
import com.google.common.base.Objects;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;

import java.util.*;

public class Building {

    @Id
    private int id;

    private BuildingType buildingType;
    private int level;
    private Location location;
    // TODO http://stackoverflow.com/questions/30970717/specify-field-is-transient-for-mongodb-but-not-for-restcontroller
    @Transient
    private int hp;
    @Transient
    private BuildingDefense defenses;

    private static Map<Building,Building> buildings = new HashMap<>();

    public Building(){}
    public Building(BuildingType buildingType, int level){
        this.buildingType = buildingType;
        this.level = level;
        if(BuildingConstants.hpMap.containsKey(buildingType))
            this.hp = BuildingConstants.hpMap.get(buildingType).get(level - 1);

        this.defenses = BuildingDefense.get(buildingType, level);
    }

    public static Building get(BuildingType townHall, int level) {
        Building newCandidate = new Building(townHall,level);
        if (buildings.containsKey(newCandidate)){
            return buildings.get(newCandidate);
        }
        buildings.put(newCandidate,newCandidate);
        return newCandidate;
    }

    public int getId() {
        return id;
    }

    public String getName(){
        return buildingType.getName();
    }

    public BuildingType getBuildingType() {
        return buildingType;
    }

    public int getHp() {
        return hp;
    }

    public int getLevel() {
        return level;
    }

    public Location getLocation() {
        return location;
    }

    public void setLocation(Location location) {
        this.location = location;
    }

    public BuildingDefense getDefenses() {
        return defenses;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Building building = (Building) o;
        return Objects.equal(id, building.id) &&
                Objects.equal(hp, building.hp) &&
                Objects.equal(level, building.level) &&
                Objects.equal(buildingType, building.buildingType) &&
                Objects.equal(defenses, building.defenses) &&
                Objects.equal(location, building.location);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id, buildingType, hp, level, defenses, location);
    }
}

As is, hp and defenses show up as 0 and null respectively. If I remove the @Transient tag it comes through.

Carlos Bribiescas
  • 4,197
  • 9
  • 35
  • 66

7 Answers7

10

As long as you use org.springframework.data.annotation.Transient it should work as expected. Jackson knows nothing about spring-data and it ignores it's annotations.

Sample code, that works:

interface PersonRepository extends CrudRepository<Person, String> {}
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;

@Document
class Person {
    @Id
    private String id;
    private String name;
    @Transient
    private Integer age;

    // setters & getters & toString()
}
@RestController
@RequestMapping("/person")
class PersonController {
    private static final Logger LOG = LoggerFactory.getLogger(PersonController.class);
    private final PersonRepository personRepository;

    @Autowired
    PersonController(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @RequestMapping(method = RequestMethod.POST)
    public void post(@RequestBody Person person) {
        // logging to show that json deserialization works
        LOG.info("Saving person: {}", person);
        personRepository.save(person);
    }

    @RequestMapping(method = RequestMethod.GET)
    public Iterable<Person> list() {
        Iterable<Person> list = personRepository.findAll();
        // setting age to show that json serialization works
        list.forEach(foobar -> foobar.setAge(18));

        return list;
    }
}

Executing POST http://localhost:8080/person:

{
    "name":"John Doe",
    "age": 40
}
  • Log output Saving person: Person{age=40, id='null', name='John Doe'}
  • Entry in person collection: { "_id" : ObjectId("55886dae5ca42c52f22a9af3"), "_class" : "demo.Person", "name" : "John Doe" } - age is not persisted

Executing GET http://localhost:8080/person:

  • Result: [{"id":"55886dae5ca42c52f22a9af3","name":"John Doe","age":18}]
Maciej Walkowiak
  • 12,372
  • 59
  • 63
4

The problem for you seems to be that both mongo and jackson are behaving as expected. Mongo does not persist the data and jackson ignores the property since it is marked as transient. I managed to get this working by 'tricking' jackson to ignore the transient field and then annotating the getter method with @JsonProperty. Here is my sample bean.

    @Entity
    public class User {

    @Id
    private Integer id;
    @Column
    private String username;

    @JsonIgnore
    @Transient
    private String password;

    @JsonProperty("password")
    public String getPassword() {
        return // your logic here;
    }
}

This is more of a work around than a proper solution so I am not sure if this will introduce any side effects for you.

ArunM
  • 2,274
  • 3
  • 25
  • 47
  • I think this does the reverse. I want the Fields in my JSON object. I do not want MongoDB to persist it. Or does `JsonIgnoreProperties` mean that it will ignore the `@Transient` annotation? – Carlos Bribiescas Jun 22 '15 at 13:20
  • Have completely reworked my answer... There seems to be no proper way of doing this .. – ArunM Jun 22 '15 at 18:59
  • Hmm, this isn't working for me. What version of mongo and spring-boot are you using? – Carlos Bribiescas Jun 24 '15 at 23:27
4

I solved by using @JsonSerialize. Optionally you can also opt for @JsonDeserialize if you want this to be deserailized as well.

@Entity
public class Article {

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

@Transient
@JsonSerialize
@JsonDeserialize
private Boolean testing;
}

// No annotations needed here
public Boolean getTesting() {
    return testing;
}

public void setTesting(Boolean testing) {
    this.testing = testing;
}
sheetal
  • 3,014
  • 2
  • 31
  • 45
2

carlos-bribiescas, what version are you using for it. It could be version issue. Because this transient annotation is meant only for not persisting to the mongo db. Please try to change the version.Probably similar to Maciej one (1.2.4 release)

There was issue with json parsing for spring data project in one of the version. http://www.widecodes.com/CyVjgkqPXX/fields-with-jsonproperty-are-ignored-on-deserialization-in-spring-boot-spring-data-rest.html

sach
  • 271
  • 1
  • 7
2

Since you are not exposing your MongoRepositories as restful endpoint with Spring Data REST it makes more sense to have your Resources/endpoint responses decoupled from your domain model, that way your domain model could evolve without impacting your rest clients/consumers. For the Resource you could consider leveraging what Spring HATEOAS has to offer.

iamiddy
  • 3,015
  • 3
  • 30
  • 33
  • I am exposing my Mongo Repos through a REST though... This is just the domain model for what I save both in mongo and what I use on my front end. – Carlos Bribiescas Jul 03 '15 at 15:32
  • I understand but , y're not using Spring Data Rest for this, and you won`t run into issues like these, by seperating the domain model from resource/Rest model – iamiddy Jul 03 '15 at 20:39
2

You can use the annotation org.bson.codecs.pojo.annotations.BsonIgnore instead of @transient at the fields, that MongoDB shall not persist.

@BsonIgnore
private BuildingDefense defenses;

It also works on getters.

Thomas Schütt
  • 832
  • 10
  • 14
0

I solved this question by implementing custom JacksonAnnotationIntrospector:

@Bean
@Primary
ObjectMapper objectMapper() {
  Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
  AnnotationIntrospector annotationIntrospector = new JacksonAnnotationIntrospector() {
    @Override
    protected boolean _isIgnorable(Annotated a) {
      boolean ignorable = super._isIgnorable(a);
      if (ignorable) {
        Transient aTransient = a.getAnnotation(Transient.class);
        JsonIgnore jsonIgnore = a.getAnnotation(JsonIgnore.class);

        return aTransient == null || jsonIgnore != null && jsonIgnore.value();
      }
      return false;
    }
  };
  builder.annotationIntrospector(annotationIntrospector);
  return builder.build();
}

This code makes invisible org.springframework.data.annotation.Transient annotation for Jackson but it works for mongodb.

Alex Po
  • 1,837
  • 1
  • 24
  • 28