I have these two classes:
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id",scope = Rol.class)
public class Rol extends MyEntity implements Serializable {
private Integer id;
private String rolName;
public Rol(Integer id, String rolName) {
this.id = id;
this.rolName = rolName;
}
...
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id",scope = User.class)
public class User extends MyEntity implements Serializable {
private Integer id;
private String name;
private List<Rol> rolList;
public User(Integer id, String name, List<Rol> rolList) {
this.id = id;
this.name = name;
this.rolList = rolList;
}
...
}
and I try to serialize and deserialize the user object as following
Rol rol1 = new Rol(1, "MyRol");
Rol rol2 = new Rol(1, "MyRol");
List<Rol> rolList = new ArrayList();
rolList.add(rol1);
rolList.add(rol2);
user = new User(1, "MyUser", rolList);
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(user);
User userJson = mappe.readValue(jsonString, User.class);
and the JsonMappingException: Already had POJO for id is produced. Why?
When I review the json result of the serialization I see that the result is
{"id": 1,"name": "MyName","rolList": [{"id": 1,"rolName": "MyRol"},{"id": 1,"rolName": "MyRol"}]}
when the result should be
{"id": 1,"name": "MyName","rolList": [{"id": 1,"rolName": "MyRol"},1]}
because rol1 and rol2 are different instances of the same POJO identifier with id 1.
How can I avoid the JsonMappingException? In my project I have some different instances of the same POJO. I can guarantee that if the id's are equal -> objects are equal.
Excuse me for my bad English.
For anyone returning to this question, it looks like there's option to do this with a custom ObjectIdResolver in Jackson. You can specify this on the #JsonIdentityInfo annotation, e.g. :
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "name",
resolver = CustomObjectIdResolver.class)
Then perhaps wrap the normal SimpleObjectIdResolver class to get going and customise bindItem().
In my case I wanted to avoid overlapping objectIds, so cleared down the references when I started a new Something:
public class CustomObjectIdResolver implements ObjectIdResolver {
private ObjectIdResolver objectIdResolver;
public CustomObjectIdResolver() {
clearReferences();
}
#Override
public void bindItem(IdKey id, Object pojo) {
// Time to drop the references?
if (pojo instanceof Something)
clearReferences();
objectIdResolver.bindItem(id, pojo);
}
#Override
public Object resolveId(IdKey id) {
return objectIdResolver.resolveId(id);
}
#Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == getClass();
}
#Override
public ObjectIdResolver newForDeserialization(Object context) {
return new CustomObjectIdResolver();
}
private void clearReferences() {
objectIdResolver = new SimpleObjectIdResolver();
}
}
Jackson expects in this case different id for different class instances. There has been a previous discussion at github here. Overriding hashCode and equals will not help. Object references must match for equal id.
Options
Reuse Rol instances instead of making new ones with equal fields. As a bonus you will also save memory.
Modify the application logic so that it doesn't depend on #JsonIdentityInfo
Related
I'm using Jackson mixins to only serialize out specific fields.
My ObjectMapper is configured like so:
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setSerializationInclusion(Include.NON_NULL);
mapper.addMixIn(Person.class, SyncPerson.class);
mapper.addMixIn(TransactionLog.class, TransactionLogExport.class);
Here are the model classes paired with the JSON mixin objects that I'd like to export:
// Model class
public class Person {
private Long id;
private String email;
private String firstName;
private String lastName;
}
// Desired JSON format. Excludes 'id' field
public interface SyncPerson {
#JsonProperty("firstName")
String getFirstName();
#JsonProperty("lastName")
String getLastName();
#JsonProperty("email")
String getEmail();
}
// Model class
public class TransactionLog {
private long id;
private Integer version;
private Person person;
private Date date;
private EntityAction action;
}
// Desired JSON format. Excludes 'id' field, 'version', 'date'
public interface TransactionLogExport {
#JsonProperty("id")
String getId();
#JsonProperty("person")
Person person();
#JsonProperty("action")
EntityAction getAction();
}
Yet, my tests are showing that the person attribute of the TransactionLog isn't coming through.
#Test
public void testWriteValue() throws Exception {
Person person = new Person();
person.setEmail("a#c.com");
person.setFirstName("A");
person.setLastName("C");
TransactionLog log = new TransactionLog();
log.setId(0L);
log.setAction(EntityAction.CREATE);
log.setPerson(person);
log.setStartValue("start");
log.setEndValue("end");
log.setChanges("change");
String prettyJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(log);
System.out.println(prettyJson);
// Prints:
// {
// "id" : 0,
// "action" : "CREATE",
}
}
If I try the same test with a regular ObjectMapper mapper = new ObjectMapper(); instead of the mixin, then I see the full object exported, including the Person with email, names, etc. So something must be wrong with how I've configured the mixin... or else I'm misunderstanding something.
So can anyone help indicate what I could do to export out the subtype 'person' in my mixin?
Thanks!
Finally figured out the issue. The test now prints what we want:
{
“id” : 0,
“person” : {
“email” : “a#c.com”,
“firstName” : “A”,
“lastName” : “C”
},
“action” : “CREATE”
}
The mistake was in TransactionLogExport. It needs to say:
#JsonProperty("person")
Person getPerson();
Instead of:
#JsonProperty("person")
Person person();
I.e. the method needs to start with 'get'.
I have the following Entities
public class Manufacturer
{
int id;
String name;
Country country;
List<Model> models;
}
public class Model
{
int id;
String name;
}
And the following DTO
public class ManufacturerLastModelDto
{
Integer id;
String name;
ModelDto model;
}
public class ModelDto
{
int id;
String name;
}
Now I want to map the Manufacturer to the ManufacturerLastModelDto, like that:
modelMapper.map(manufacturer, ManufacturerLastModelDto.class)
So that only the first entry of the List model will be assigned from manufacturer.
My previous solution was that I had a List of ModelDto's even in the DTO and removed all Entries after Index 0. That was OK, because the ModelMapper mapped the child from Model to ModelDTO automatically.
But only Response wasn't so nice:
models: [
{...}
]
because it was sent as an Array.
Do I need a custom ModelMapper here? If so, how to build it? The tutorial is really complex. Do I need a converter or a TypeMap (or both)?
I am not too familiar with ModelMapper but have used it occasionally.
Yes, you would need to create a converter for the property, and you could either use it with the ModelMapper or a TypeMap. Only caveat being that you will need to map the property yourself, for instance
The converter
Converter<List<Model>, ModelDto> modelConverter = new AbstractConverter<List<Model>, ModelDto>() {
#Override
protected ModelDto convert(List<Model> models) {
if (models == null || models.isEmpty()) {
return null;
}
Model model = models.get(0);
ModelDto dto = new ModelDto();
dto.setId(model.getId());
dto.setName(model.getName());
return dto;
}
};
Now using default model mapper
ModelMapper modelMapper = new ModelMapper();
modelMapper.addConverter(modelConverter);
ManufacturerLastModelDto result = modelMapper.map(manufacturer, ManufacturerLastModelDto.class);
Or using the TypeMap
TypeMap<Manufacturer, ManufacturerLastModelDto> typeMap = modelMapper.typeMap(Manufacturer.class, ManufacturerLastModelDto.class)
.addMappings(mapper ->
mapper.using(modelConverter).map(Manufacturer::getModels, ManufacturerLastModelDto::setModel)
);
ManufacturerLastModelDto result = typeMap.map(manufacturer);
I have two classes Athlete and Injury, the last one contains Athlete object, when the serialization happens I get the following JSON representation back:
{"id":X,"kindOfInjury":"...","muscle":"...","side":"...","outOfTrainig":Y,"injuryDate":"2018-Jun-02","athlete":{"id":X,"firstName":"...","lastName":"...","age":X,"email":"..."}}
I don't want to get all the information about Athlete - just an id value, like "athleteId":1, instead of getting the entire object representation.
So, I have found that I need to apply my custom Serializer which implements StdSerializer on Injury class. So this is what I got so far:
class InjurySerializer extends StdSerializer<Injury> {
public InjurySerializer() {
this(null);
}
public InjurySerializer(Class<Injury> i) {
super(i);
}
#Override
public void serialize(
Injury value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeNumberField("id", value.getId());
jgen.writeStringField("kindOfInjury", value.getKindOfInjury());
jgen.writeStringField("muscle", value.getMuscle());
jgen.writeStringField("side", value.getSide());
jgen.writeNumberField("outOfTraining", value.getOutOfTraining());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MMM-dd");
Date date = new Date();
String ourformat = formatter.format(date.getTime());
jgen.writeStringField("injuryDate", ourformat);
jgen.writeNumberField("athleteId", value.getAthlete().getId());
jgen.writeEndObject();
}
}
And the actual Injury class:
#Entity
#Table(name = "INJURY")
#JsonSerialize(using = InjurySerializer.class)
public class Injury {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "INJURY_ID")
private Long id;
#Column(name = "KIND_OF_INJURY")
private String kindOfInjury;
#Column(name = "MUSCLE")
private String muscle;
#Column(name = "SIDE")
private String side;
#Column(name = "OUT_OF_TRAINING")
private Integer outOfTraining;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MMM-dd")
#Column(name = "INJURY_DATE")
private Date injuryDate;
#ManyToOne
#JoinColumn(name = "ATHLETE_ID")
private Athlete athlete;
So, this solution works, but it looks terrible...
Question is the following:
1) Is there any mechanism which provides me functionality to change the serialization of only ONE property which I really need, instead of writing all this tedious code, where the actual change is only in this line? :
jgen.writeNumberField("athleteId", value.getAthlete().getId());
2) Could you recommend me something to read about Jackson because at this point I have a little bit mess in my head about it?
Thanks for the patience and I'm looking forwards for your responses :)
You can use the Data Transfer Object (DTO) for that purposes.
Create a simple POJO like this:
public class InjuryDTO {
//all other required fields from Injury model...
#JsonProperty("athlete_id")
private Long athleteId;
}
And converter for it:
#Component
public class InjuryToDTOConverter{
public InjuryDTO convert(Injury source){
InjuryDTO target = new InjuryDTO();
BeanUtils.copyProperties(source, target); //it will copy fields with the same names
target.setAthleteId(source.getAthlete().getId());
return target;
}
}
You can use it like that:
#RestController("/injuries")
public class InjuryController {
#Autowired
private InjuryToDTOConverter converter;
#Autowired
private InjuryService injuryService;
#GetMapping
public InjuryDTO getInjury(){
Injury injury = injuryService.getInjury();
return converter.convert(injury);
}
}
The benefit of this approach is that you can have multiple DTOs for different purposes.
You might find it less tedious to use the #JsonIgnore annotation instead of writing a custom serializer. Take this example
public class Person {
private int id;
#JsonIgnore
private String first;
#JsonIgnore
private String last;
#JsonIgnore
private int age;
// getters and setters omitted
}
When Jackson serializes this class, it only includes the "id" property in the resulting JSON.
#Test
void serialize_only_includes_id() throws JsonProcessingException {
final var person = new Person();
person.setId(1);
person.setFirst("John");
person.setLast("Smith");
person.setAge(22);
final var mapper = new ObjectMapper();
final var json = mapper.writeValueAsString(person);
assertEquals("{\"id\":1}", json);
}
You can try manupulating json string using basic string replace method.
I ran your json and converted it to your desired format:
public static void main(String args[]) {
String json = "{\"id\":123,\"kindOfInjury\":\"...\",\"muscle\":\"...\",\"side\":\"...\",\"outOfTrainig\":Y,\"injuryDate\":\"2018-Jun-02\",\"athlete\":{\"id\":456,\"firstName\":\"...\",\"lastName\":\"...\",\"age\":14,\"email\":\"...\"}}";
JsonObject injury = new JsonParser().parse(json).getAsJsonObject();
JsonObject athelete = new JsonParser().parse(injury.get("athlete").toString()).getAsJsonObject();
String updateJson = injury.toString().replace(injury.get("athlete").toString(), athelete.get("id").toString());
updateJson = updateJson.replace("athlete", "athleteId");
System.out.println(updateJson);
}
output:
{"id":123,"kindOfInjury":"...","muscle":"...","side":"...","outOfTrainig":"Y","injuryDate":"2018-Jun-02","athleteId":456}
Dependency:
implementation 'com.google.code.gson:gson:2.8.5'
If you can replace with regex that will be bit more cleaner.
I have in my controller:
#RestController
public class OneTwoController {
private OnTwoService _service;
//... more code
#PostMapping("/api/one-two")
#CrossOrigin
public ResponseEntity<ServiceResponse> save(#RequestBody OneTwo model) {
return ResponseEntity.ok().body( _service.Save(model));
}
In my entity:
#Entity(name = "OneTwo")
#Where (clause = "deleted='false'")
public class OneTwo{
#EmbeddedId
private OneTwoKey_id;
public OneTwo(OneTwoKey id) {
this._id = id;
}
#JsonProperty("oneTwo")
public void setId(OneTwoKey value) {
this._id = value;
}
The OneTwoKey class:
public class OneTwoKey implements Serializable {
#Column(name = "OneID")
private int _oneID;
#Column(name = "TwoID")
private int _twoID;
public OneTwoKey(int oneID, int twoID) {
this._oneID = oneID;
this._twoID = twoID;
}
}
The json that I send to the Rest API:
{
"oneTwo": {
"oneID": 83,
"twoID": 69
},
"deleted": true
}
The issue is that both ids arrive null, so the service can't do the insert on the DB.
How can I deal with those cases when the ids are more than one?
Try adding setters in the OneTwoKey class to make it easier for the JSON deserializer:
#JsonProperty("oneID")
public void setOneID(int oneID) {
this._oneID = oneID;
}
#JsonProperty("twoID")
public void setTwoID(int twoID) {
this._twoID = twoID;
}
Another solution is to create a DTO, use it to receive the data in the controller and then convert it to your entity:
public class OneTwoDTO {
private Map<String, Int> oneTwo;
private boolean deleted;
// setters & getters
}
Simply what you can do is instead of using
public ResponseEntity<ServiceResponse> save(#RequestBody OneTwo model) {
you can use
public ResponseEntity<ServiceResponse> save(#RequestBody String model) {
Now convert the String to json and get all the key value pairs, it would be easier if you have dynamic number of variables and you want to capture them all.
or you can use tools like jsonschema2pojo whick take a json schema and generate a pojo. In the json schema if you set
"additionalProperties": true
you can capture all the values.
Could you make sure the problem is not because of case sensitivity?
Lower case the column names. Also could you use public access on those variables as well? These are my initial guesses as to why the payload is not being binded correctly.
public class OneTwoKey implements Serializable {
#Column(name = "oneID")
public int _oneID;
#Column(name = "twoID")
public int _twoID;
I have a JPA entity with a List of custom objects as one of its fields. Using a Jackson converter, I've managed to persist this list as a JSON array into a MySQL database, but Iam unable to insert into this list after its initial creation.
I can successfully retrieve the existing list, add a new object in memory(and test that it has been inserted), then save it via a Spring REST repository. However, it never seems to persist. Any ideas? Here is my code (this is a Spring Boot project FYI):
Candidate entity with a List inside
#Entity
#Table(name = "Candidates", schema = "Candidate")
public class Candidate extends ResourceSupport {
#Id
#Column(name = "CandidateID")
private Long candidateID;
// More fields
#Column(name = "Fields")
#Convert(converter = CollectionConverter.class)
private List<CandidateField> fields;
//Getters & setters
}
CandidateField class which makes up the List above. The CandidateField is simply a POJO that models the JSON stored in a single field in the Candidate table, it is not an independent entity.
public class CandidateField {
private Long fieldID;
private String name;
private boolean current;
public CandidateField () {
}
public CandidateField (Long fieldID, String name, boolean current) {
this.fieldID = fieldID;
this.name = name;
this.current = current;
}
//Getters & Setters
}
Converter
public class CollectionConverter implements AttributeConverter<List<CandidateField>, String> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convertToDatabaseColumn(List<CandidateField> object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
e.printStackTrace();
return "";
}
}
#Override
public List<CandidateField> convertToEntityAttribute(String data) {
try {
return objectMapper.readValue(data, new TypeReference<List<CandidateField>>() {});
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
Code that persists to database
public void addField(Long fieldID, Long candidateID) {
Candidate candidate = repository.findOne(candidateID);
candidate.getFields().add(new CandidateField(fieldID, "", true));
repository.saveAndFlush(candidate);
}
Repository
#RepositoryRestResource
public interface CandidateRepository extends JpaRepository<Candidate,Long>{}
I can't seem to figure out why this won't persist. Any help will be very much appreciated. Cheers!
Consider defining the cascade type for your collection.
When you persist your Candidate objects the operation is not cascaded by default and thus you need to define it yourself unless you persist your CandidateField objects directly.