Self-referencing JSON with Spring Data REST
In a Spring Data REST data model with Lombok, how can we export and import data to self-referencing JSON without creating a parallel entities or DTOs?
For example, a self-referencing JSON using JSONPath for a simple music manager might look like:
{ "id" : 1,
"albums" : [ {
"id" : 1,
"title" : "Kind Of Blue",
"artist" : "$..artists[?(#.id=1)]",
"tracks" : [ {
"id" : 1,
"title" : "So What",
"duration" : "PT9M5S",
"musicians" : [ {
"musician" : "$..artists[?(#.id=1)]",
}, {
"musician" : "$..artists[?(#.id=2)]",
} ]
}, {
"id" : 3,
"title" : "Blue in Green",
"duration" : "PT5M29S",
"musicians" : [ {
"musician" : "$..artists[?(#.id=1)]",
}, {
"musician" : "$..artists[?(#.id=2)]",
} ]
} ]
} ],
"artists" : [ {
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 2,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
}
How can we create the import and export functionality for this representation while retaining the Spring Data REST HATEOAS functionality? The musicians container in an export/import has an array of string JSONPath expressions and in the REST APIs the musicians is an array of Person objects (see below) -- so how can Jackson be configured to select the correct serializer and deserializer the export and import operations?
Details: music manager example
Here's a Spring Boot 1.5, Spring Data Rest, Lombok, and Jackson implementation (GitHub).
Music
#Data
#NoArgsConstructor
#Entity
public class Music {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
private List<Album> albums;
#OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
private List<Person> artists;
}
Album
#Data
#NoArgsConstructor
#Entity
public class Album {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String title;
#ManyToOne(cascade = { CascadeType.ALL })
private Person artist;
#OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
private List<Track> tracks;
}
Track
#Data
#NoArgsConstructor
#Entity
public class Track {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String title;
#JsonSerialize(using = MyDurationSerializer.class)
private Duration duration;
#ManyToMany(cascade = { CascadeType.ALL })
private List<Person> musicians;
public Track(String title, String duration, List<Person> musicians) {
this.title = title;
this.duration = Duration.parse(duration);
this.musicians = musicians;
}
}
Person
#Data
#NoArgsConstructor
#Entity
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
#JsonSerialize(using = MyLocalDateSerializer.class)
private LocalDate birthDate;
public Person(String firstName, String lastName, String birthDate) {
this.firstName = firstName;
this.lastName = lastName;
this.birthDate = LocalDate.parse(birthDate);
}
}
MyDurationSerializer
public class MyDurationSerializer extends StdSerializer<Duration> {
private static final long serialVersionUID = 1L;
protected MyDurationSerializer() {
super(Duration.class);
}
#Override
public void serialize(Duration value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.toString());
}
}
MyLocalDateSerializer
public class MyLocalDateSerializer extends StdSerializer<LocalDate> {
private static final long serialVersionUID = 1L;
private DateTimeFormatter FORMATTER = ofPattern("yyyy-MM-dd");
protected MyLocalDateSerializer() {
super(LocalDate.class);
}
#Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.format(FORMATTER));
}
}
Spring Data REST HATEOAS representation
curl http://localhost:4000/albums/1/tracks
{ "id" : 1,
"albums" : [ {
"id" : 1,
"title" : "Kind Of Blue",
"artist" : "#{
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
},
"tracks" : [ {
"id" : 1,
"title" : "So What",
"duration" : "PT9M5S",
"musicians" : [ {
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 2,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
}, {
"id" : 3,
"title" : "Blue in Green",
"duration" : "PT5M29S",
"musicians" : [ {
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 2,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
} ]
} ],
"artists" : [ {
"id" : 15,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 16,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
}
Related
I try to call following webclient query:
return webClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/geocode/json")
.queryParam("key", google.getApiKey())
.queryParam("latlng", String.join(
",",
String.valueOf(point.getLat()),
String.valueOf(point.getLng()))
)
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::isError, RestErrorHandler::manageError)
.bodyToMono(*PlacesSearchResponse.class*);
REST operation from google returns (https://developers.google.com/maps/documentation/geocoding/overview#GeocodingResponses) following object:
{
"results" : [
{
"address_components" : [
{
"long_name" : "1600",
"short_name" : "1600",
"types" : [ "street_number" ]
}
],
"formatted_address" : "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
"geometry" : {
"location" : {
"lat" : 37.4224764,
"lng" : -122.0842499
},
"location_type" : "ROOFTOP",
"viewport" : {
"northeast" : {
"lat" : 37.4238253802915,
"lng" : -122.0829009197085
},
"southwest" : {
"lat" : 37.4211274197085,
"lng" : -122.0855988802915
}
}
},
"place_id" : "ChIJ2eUgeAK6j4ARbn5u_wAGqWA",
"plus_code": {
"compound_code": "CWC8+W5 Mountain View, California, United States",
"global_code": "849VCWC8+W5"
},
"types" : [ "street_address" ]
}
],
"status" : "OK"
}
And problem is that I cannot parse it into my class: PlacesSearchResponse.class
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class PlacesSearchResponse {
public String status;
public String errorMessage;
#JsonProperty(value = "results")
public List<PlacesSearchResult> results;
}
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class PlacesSearchResult implements Serializable{
private static final long serialVersionUID = 1L;
#JsonProperty(value = "address_components")
public AddressComponent addressComponents[];
}
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class AddressComponent implements Serializable {
private static final long serialVersionUID = 1L;
public String longName;
public String shortName;
#JsonProperty(value = "types")
public String[] types;
}
I cannot find any mistake and webclient is ok, because when I tried bodyToMono(String.class) then I saw this object correctly.
I think you should parse it into an array, so do the following:
...
.bodyToMono(PlacesSearchResponse[].class);
Finnaly I have following call with objects:
.bodyToMono(PlacesSearchResponse.class);
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class PlacesSearchResponse {
public String status;
public String errorMessage;
List <PlacesSearchResult> results;
}
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class PlacesSearchResult implements Serializable{
#JsonProperty("place_id")
String placeId;
#JsonProperty("address_components")
List<AddressComponent> addressComponents;
#JsonProperty("formatted_address")
String formattedAddress;
Creating index via MongoShell
db.car.createIndex({brand:1 , model:1 , colour:1 ,fuelTypes:1},{unique:true})
Creating CompoundIndex via spring application
#Document
#CompoundIndex(def = "{ 'brand':1 , 'model':1 , 'colour':1 , 'fuelTypes':1 }",unique = true)
public class Car {
private String brand;
private String model;
private List<FuelType> fuelTypes;
private String colour;
}
I was able to create via Mongo shell but not thourgh spring application.What's wrong in the above code?Are n't they equivalent?
I checked After inserting atleast one document.
Thanks in advance.
Here is a working example I tried (creates a new collection, document and the compound index):
The Car POJO class:
#CompoundIndex(name = "car-cmp-idx", def = "{'brand': 1, 'model': 1}", unique = true)
#Document
public class Car {
private String brand;
private String model;
private String colour;
public Car() {
}
public Car(String brand, String model, String colour) {
this.brand = brand;
this.model = model;
this.colour = colour;
}
// get/set methods. etc...
}
The application code to create a document in the (new) car: collection:
MongoOperations ops = new MongoTemplate(MongoClients.create(), "test");
Car car = new Car("Ford", "Model T", "Black");
ops.insert(car);
The result document verified from the mongo shell:
{
"_id" : ObjectId("5ed46f4960c3f13e5edf43b6"),
"brand" : "Ford",
"model" : "Model T",
"colour" : "Black",
"_class" : "com.example.demo.Car"
}
The indexes:
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.car"
},
{
"v" : 2,
"unique" : true,
"key" : {
"brand" : 1,
"model" : 1
},
"name" : "car-cmp-idx",
"ns" : "test.car"
}
]
Here i'm using MongoRepository and i need to query a list of objects that includes certain id in an array of objects inside.
The document structure :
{
"_id" : ObjectId("5ccc1c54a3d5eed9a6b8015a"),
"email" : "sineth3#gmail.com",
"name" : "edward3",
"businessName" : "aroma3",
"phone" : "07177222233",
"address" : "no 100 NY",
"bookletSignups" : [
{
"bookletId" : "sample-booklet",
"contactName" : "john doe"
},
{
"bookletId" : "sample-booklet1",
"contactName" : "john doe1"
}
],
"eventSignups" : [
{
"eventId" : "sample-event",
"contactName" : "john doe2"
},
{
"eventId" : "sample-event 1",
"contactName" : "john doe3"
}
],
"infoSignups" : [
{
"infoRequestId" : "sample-info ",
"contactName" : "john doe4"
},
{
"infoRequestId" : "sample-event 1",
"contactName" : "john doe5"
}
],
"webinarSignups" : [
{
"webinarId" : "sample-webinar ",
"contactName" : "john doe6"
},
{
"webinarId" : "sample-webinar 1",
"contactName" : "john doe7"
}
],
"updatedTime" : ISODate("2016-03-03T08:00:00Z")
}
The Repository :
#Repository
public interface UserRepository extends MongoRepository<User,String> {
#org.springframework.data.mongodb.repository.Query(value = "{ 'bookletSignups': { $elemMatch: { 'bookletSignups.bookletId' : ?0 } }}")
List<User> findByBookletId(String id);
}
User model class:
#Id
private String id;
private String email;
private String name;
private String businessName;
private String phone;
private String address;
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Date createdTime;
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Date updatedTime;
#Field("bookletSignups")
#DBRef
private List<BookletSignUp> bookletSignups;
#Field("eventSignups")
#DBRef
private List<EventSignUp> eventSignups;
#Field("infoSignups")
#DBRef
private List<InfoSignUp> infoSignups;
#Field("webinarSignups")
#DBRef
private List<WebinarSignUp> webinarSignups;
So im trying to retrieve User objects that includes a bookletSignups object with the passing bookletId value. But the result is empty. What has gone wrong here?
I would say you need to modify your query to looks like this:
#Repository
public interface UserRepository extends MongoRepository<User,String> {
#org.springframework.data.mongodb.repository.Query(value = "{ 'bookletSignups': { $elemMatch: { 'bookletId' : ?0 } }}")
List<User> findByBookletId(String id);
}
If you check MongoDB documentation for $elemMatch, link to documentation, you can see that basically in $elemMatch operator you are using field in embedded object, so you don't need to specify again name of array in which you are searching for objects.
I'm using Spring Boot 2.x, Spring Data REST, Spring HATEOAS.
I've a simple bean:
#Entity
public class Printer extends AbstractEntity {
#NotBlank
#Column(nullable = false)
private String name;
// Ip address or hostname
#NotBlank
#Column(nullable = false)
private String remoteAddress;
#NotNull
#Enumerated(EnumType.STRING)
#Column(nullable = false)
#JsonSerialize(using = PrinterModelSerializer.class)
private PrinterModel model;
#NotNull
#Column(nullable = false, columnDefinition = "BIT DEFAULT 0")
private boolean ssl = false;
// The store where the device is connected
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Store store;
I added a custom Serializer for the enum PrinterModel:
public class PrinterModelSerializer extends JsonSerializer<PrinterModel> {
#Override
public void serialize(PrinterModel value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject(); // {
gen.writeStringField("key", value.toString());
gen.writeStringField("name", value.getName());
gen.writeBooleanField("fiscal", value.isFiscal());
gen.writeStringField("imageUrl", value.getImageUrl());
gen.writeEndObject(); // }
gen.close();
}
}
When I get the resource printer using Spring Data REST repositories I've:
{
"sid" : "",
"createdBy" : "admin",
"createdDate" : "2018-10-16T12:29:24Z",
"lastModifiedDate" : "2018-10-16T14:12:08.671566Z",
"lastModifiedBy" : "ab48d95f-09f3-40ba-b8ba-e6fd206a2fe6",
"createdByName" : null,
"lastModifiedByName" : null,
"name" : "m30",
"remoteAddress" : "111.222.333.456",
"model" : {
"key" : "EPSON_M30",
"name" : "Epson TM-m30",
"fiscal" : false,
"imageUrl" : "https://www.epson.it/files/assets/converted/550m-550m/0/0/1/d/001d0815_pictures_hires_en_int_tm-m30_w_frontpaperloading_paper.tif.jpg"
}
}
As you can see I don't have HATEOAS links. If I remove my custom serializer instead I've the right reply:
{
"sid" : "",
"createdBy" : "admin",
"createdDate" : "2018-10-16T12:29:24Z",
"lastModifiedDate" : "2018-10-16T14:12:08.671566Z",
"lastModifiedBy" : "ab48d95f-09f3-40ba-b8ba-e6fd206a2fe6",
"createdByName" : null,
"lastModifiedByName" : null,
"name" : "m30",
"remoteAddress" : "111.222.333.456",
"model" : "EPSON_M30",
"ssl" : true,
"_links" : {
"self" : {
"href" : "http://x.x.x.x:8082/api/v1/printers/3"
},
"printer" : {
"href" : "http://x.x.x.x:8082/api/v1/printers/3{?projection}",
"templated" : true
},
"store" : {
"href" : "http://x.x.x.x:8082/api/v1/printers/3/store{?projection}",
"templated" : true
}
}
}
How can I prevent #JsonSerialize to break HATEOAS links?
Akin to my earlier question, I'm trying to access data in MongoDB using Spring REST.
I have collections of simple Key-Value Pairs and can access those fine.
{
"_id" : ObjectId("5874ab4a19b38fb91fbb234f"),
"roID" : "7ed3f9a6-bb9b-4d16-8d1a-001b7ec40b51",
"Name" : "[REDACTED]"
}
The problem is, these objects are used in another collection that displays a relationship with properties between them, like this:
{
"_id" : ObjectId("5874ab4f19b38fb91fbb6180"),
"[OBJECT CATEGORY A]" : {
"_id" : ObjectId("5874ab4a19b38fb91fbb257b"),
"roID" : "72f8a8b5-71a7-40ac-b1ac-1ffc98a507ba",
"Name" : "[REDACTED]"
},
"[OBJECT CATEGORY B]" : {
"_id" : ObjectId("5874ab4b19b38fb91fbb32a3"),
"roID" : "919446ab-1898-419f-a704-e8c34985f945",
"Name" : "[REDACTED]"
},
"[RELATIONSHIP INFORMATION]" : [
{
"[PROPERTY A]" : [
{
"[VALUE A]" : 5.0
},
{
"[VALUE B]" : 0.0
}
]
},
Properties are somewhere between 8 and 20.
The definition of the first (plain) object in Java looks like this:
#Document(collection="OBJ")
public class Obj {
public Obj(){};
#Id
public String id;
#Field("roID")
public String roID;
#Field("Name")
public String name;
}
The repository class:
#RepositoryRestResource(collectionResourceRel = "OBJ", path = "OBJ")
public interface ObjRepo extends MongoRepository<Obj, String> {
List<Obj> findByName(#Param("name") String name);
}
The question is: how do I access the nested objects? I've tried using LinkedHashMap in place of the Strings for the complex collection, curl only returns "null" when I try to access them. I tried defining a class
public class BITS {
#Id
private String _id;
#Field("roID")
private String roID;
#Field("Name")
private String name;
public BITS(){}
public BITS(String _id,String roID, String name){
this._id = _id;
this.roID = roID;
this.name = name;
}
}
to access these objects, unsuccessfully.
Turns out the class approach was correct, just not well executed.
I've created a plain JSON Collection for testing purposes:
#Document(collection="JSON")
public class JSON {
#Id
public String id;
#Field("squares")
public Square square;
#Field("dots")
public Dot dot;
public JSON(){};
public JSON(String id, Square square,Dot dot){
this.id = id;
this.square = square;
this.dot = dot;
};
}
Square.java
public class Square {
private String id;
private int x;
private int y;
public Square(){};
public Square(String id,int x, int y){
this.id = id;
this.x = x;
this.y = y;
};
public Map<String, Integer> getSquare()
{
Map<String, Integer> res = new HashMap<>();
res.put("x", x);
res.put("y", y);
return res;
}
}
(Dot is the same, just for the test)
So it's just about remodeling the desired response exactly, despite it being in that format in the database already.
If anyone could point me to where I can remove the clutter from the response tho, that would be nice. Currently looks like this:
"_embedded" : {
"JSON" : [ {
"square" : null,
"dot" : {
"dot" : {
"x" : 4,
"y" : 3
}
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/JSON/58ac466160fb39e5e8dc8b70"
},
"jSON" : {
"href" : "http://localhost:8080/JSON/58ac466160fb39e5e8dc8b70"
}
}
}, {
"square" : {
"square" : {
"x" : 12,
"y" : 2
}
},
"dot" : null,
"_links" : {
"self" : {
"href" : "http://localhost:8080/JSON/58ac468060fb39e5e8dc8b7e"
},
"jSON" : {
"href" : "http://localhost:8080/JSON/58ac468060fb39e5e8dc8b7e"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/JSON"
},
"profile" : {
"href" : "http://localhost:8080/profile/JSON"
}
},
"page" : {
"size" : 20,
"totalElements" : 2,
"totalPages" : 1,
"number" : 0
}
}