I'm writing an API using the HalBuilder library for HAL representations.
As it stands now, I need to have two different methods for the JSON and HAL representations. As an example, my VersionResource includes the following two methods:
#GET
#ApiOperation(value = "Find all versions", response = Version.class, responseContainer = "List")
#Produces({MediaType.APPLICATION_JSON})
public Response getAsJson() {
List<Version> versions = repository.selectAll();
return Response.ok().entity(versions).build();
}
#GET
#ApiOperation(value = "Find all versions", notes="Returns HAL format", response = Representation.class, responseContainer = "List")
#Produces({RepresentationFactory.HAL_JSON})
public Representation getAsHalJson() {
List<Version> versions = repository.selectAll();
return this.versionRepresentationFactory.createResourceRepresentation(versions);
}
(Note: I'm sure there's a better way of collapsing these methods, and I'm looking into a way to do that)
But my immediate problem is that using two methods causes duplicate entries in my Swagger documentation:
Those two GET /versions are effectively the same thing, but they have different return types, so Swagger wants them to be different.
I'd like to collapse those two. What are my options here?
[It's probably worth pointing out that I'm using the Swagger Maven plugin to generate my documentation. The application is also using Guice for DI and Jersey for JSON representations.]
I read in https://github.com/swagger-api/swagger-spec/issues/146#issuecomment-59082475:
per design, we don't overload response type definitions for the same response code.
So I think the Maven plugin creates an invalid Swagger document.
What are your options?
Be patient and watch these Swagger issues: https://github.com/swagger-api/swagger-spec/issues/146 and https://github.com/swagger-api/swagger-spec/issues/182
Don't use Swagger
Related
We are trying to create a Java client for an API created with Spring Data.
Some endpoints return hal+json responses containing _embedded and _links attributes.
Our main problem at the moment is trying to wrap our heads around the following structure:
{
"_embedded": {
"plans": [
{
...
}
]
},
...
}
When you hit the plans endpoint you get a paginated response the content of which is within the _embedded object. So the logic is that you call plans and you get back a response containing an _embedded object that contains a plans attribute that holds an array of plan objects.
The content of the _embedded object can vary as well, and trying a solution using generics, like the example following, ended up returning us a List of LinkedHashMap Objects instead of the expected type.
class PaginatedResponse<T> {
#JsonProperty("_embedded")
Embedded<T> embedded;
....
}
class Embedded<T> {
#JsonAlias({"plans", "projects"})
List<T> content; // This instead of type T ends up deserialising as a List of LinkedHashMap objects
....
}
I am not sure if the above issue is relevant to this Jackson bug report dating from 2015.
The only solution we have so far is to either create a paginated response for each type of content, with explicitly defined types, or to include a List<type_here> for each type of object we expect to receive and make sure that we only read from the populated list and not the null ones.
So our main question to this quite spread out issue is, how one is supposed to navigate such an API without the use of Spring?
We do not consider using Spring in any form as an acceptable solution. At the same time, and I may be quite wrong here, but it looks like in the java world Spring is the only framework actively supporting/promoting HAL/HATEOAS?
I'm sorry if there are wrongly expressed concepts, assumptions and terminology in this question but we are trying to wrap our heads around the philosophy of such an implementation and how to deal with it from a Java point of view.
You can try consuming HATEOS API using super type tokens. A kind of generic way to handle all kind of hateos response.
For example
Below generic class to handle response
public class Resource<T> {
protected Resource() {
this.content = null;
}
public Resource(T content, Link... links) {
this(content, Arrays.asList(links));
}
}
Below code to read the response for various objects
ObjectMapper objectMapper = new ObjectMapper();
Resource<ObjectA> objectA = objectMapper.readValue(response, new TypeReference<Resource<ObjectA>>() {});
Resource<ObjectB> objectB = objectMapper.readValue(response, new TypeReference<Resource<ObjectB>>() {});
You can refer below
http://www.java-allandsundry.com/2012/12/json-deserialization-with-jackson-and.html
http://www.java-allandsundry.com/2014/01/consuming-spring-hateoas-rest-service.html
I'm taking my first swing at creating a RESTful API with OAS 3.0, Swagger Codegen, Spring MVC, and Spring HATEOAS. I want to use the OAS definition for documentation generation and server/client stub generation and use HATEOAS to create hyperlinks related resources.
I currently have my resources extending ResourceSupport and can add my links such that the responses have the _embedded and _links fields that I would expect. My issue is how to properly map the HATEOAS Resource to the model generated by Swagger codegen. My OAS definition matches the hal+json response, so the fields are identical in the swagger model and my HATEOAS Response.
Is there a way to easily map these? I'm also willing to accept that I am interpreting this incorrectly and that these frameworks don't really mesh together.
OAS example:
responses:
200:
description: ...
content:
application/hal+json:
schema:
$ref: '#/components/schemas/OasPersonResponse'
components:
schemas:
OasPersonResponse:
type: object
properties:
firstName:
type: string
lastName:
type: string
_links:
type: object
properties:
self:
type: object
properties:
href:
type: string
Resource example:
public class PersonResource extends ResourceSupport {
private final Person person;
public PersonResource(Person person) {
this.person = person;
}
public String getFirstName() {
return person.getFirstName();
}
public String getLastName() {
return person.getLastName();
}
}
Controller Example:
#Controller
public class PersonController implements PersonApi {
#Override
public ResponseEntity<OasPersonResponse> getPersonById(Integer personId) {
Person person = someDb.getPerson(personId);
PersonResource personResource = new PersonResource(person);
personResource
.add(linkTo(methodOn(PersonController.class)
.getPersonById(personId))
.withSelfRel();
Resource<PersonResource> returnResource =
new Resource(personResource);
return new ResponseEntity<>(returnResponse, HttpStatus.OK);
}
My issue is with the stub generated by swagger codegen expecting a return type of ResponseEntity<OasPersonResponse> but have a reference to a Resource<PersonResource>. Both OasPersonResponse and PersonResource represent the same data but the OasPersonResponse explicitly defines the _links object whereas the response with the PersonResource gets serialized to have the _links object.
Is there an easy way for me to convert the HATEOAS Resource to the model that was created by swagger codegen?
Thanks in advance for the help and guidance.
I'm currently working on a very similar project!
Firstly, if you can, I'd recommend using the 1.0.0.RC1 version of spring-hateoas as it has some pretty major quality of life improvements over the 0.25.x release branch. Of major relevance is that using the EntityModel wrapper class is now the recommended practice, which means you can just leave the relations out of your base entity specification. (The downside is this reduces the immediate utility of the OpenAPI spec; I haven't quite figured out how to reconcile that yet.)
Secondly, I'm afraid there doesn't seem to be much existing work on the swagger-codegen side as far as supporting Spring HATEOAS is concerned; in fact, I keep running into annoying bugs with the plain Spring "language" generator.
So either we can write our own swagger-codegen generator for spring-hateoas, or just heavily customize some templates to get "close enough" (there's less of this needed when using the EntityModel wrapper rather than extending ResourceSupport). I've gone with the latter approach so far, for what that's worth.
I have a list of ids which I need to pass as an argument to be displayed in JSON format. Specifically, i put the values in a List and would like my GET method to return them as:
[ 1, 5, 12,...]
I tried using GenericList, but I get a MessageBodyWriter error:
MessageBodyWriter not found for media type=application/json, type=class java.util.ArrayList, genericType=java.util.List<java.lang.Long>.
I'm using Jersey 2.16, and I've had no problems outputting lists of custom classes. The code is here:
#GET
#Path("{name}")
#Produces(MediaType.APPLICATION_JSON)
public Response getUsersByName(#PathParam("name") String name){
List<Long> userIds = userService.getUserssByName(name);
GenericEntity<List<Long>> list = new GenericEntity<List<Long>>(userIds){};
return Response.ok(list).build();
}
UserService queries a hashmap for all the users whose name matches a certain name and returns a list of their keys:
public List<Long> getUserssByName(String name){
List<Long> ids = new ArrayList<>();
for (Entry<Long, User> entry : users.entrySet()){
User user = entry.getValue();
if (user.getName().equals(name)){
ids.add(entry.getKey());
}
}
return ids;
}
How can I get that list to be displayed as I've stated above?
I think this explanation-answer will serve better than just "fixing" your code
By default, jersey provides a set of entity providers that can be used for given media types. For application/json, the following types are provided by default:
byte[]
String
InputStream
Reader
File
DataSource
StreamingOutput
To add support for serialization of other types to json is a bit of a rocky slope with the tools you are currently working with. I'll explain...
Jersey is just a framework for developing RESTful web services in java. There was a time when REST didn't exist (or wasn't seen as useful) and before that concept came about there were already many concrete implementations to facilitate Http data transfer. JAX-RS was one of them. This was during a time when javascript was likely in its infancy and XML largely ruled the world wide web. JAX-RS wasn't designed to natively prevent type erasure, and type erasure is precisely what we are trying to achieve. For me to go through the extremely long-winded solution of implementing the behaviors in MessageBodyWriter would be reinventing the wheel, and no one likes to reinvent the wheel! But, if you are interested in writing your own entities, or you are interested in learning what entities are and how to customize them, head over to https://jersey.java.net/documentation/latest/message-body-workers.html. Reading this document will outline what exactly is going on with your issues.
Solution
Choose a service. Learn it, and do not reinvent the wheel. There are many services out there that can achieve this. My favorite tool is Gson. It is a Java library developed by Google to convert Java objects into their JSON representation and vice versa.
Want to know how simple this is to fix your code?
#GET
#Path("{name}")
#Produces(MediaType.APPLICATION_JSON)
public Response getUsersByName(#PathParam("name") String name){
List<Long> userIds = userService.getUserssByName(namee);
Type listType = new TypeToken<List<Long>>() {}.getType();
Gson gson = new Gson();
String userIdList = gson.toJson(userIds, listType);
return Response.ok(userIdList).build();
}
Now, jersey supports application/json for String entity responses. Pretty much all of your responses will be represented as a String, type erasure is prevented, and you become a more happy developer.
Hope this helps!
You have to convert the list to array.
#GET
#Path("{name}")
#Produces(MediaType.APPLICATION_JSON)
public Response getUsersByName(#PathParam("name") String name){
List<Long> userIds = userService.getUserssByName(namee);
Long[] userIdArray = userIds.toArray();
return Response.ok(userIdArray).build();
}
I am looking to get the key and value to each Json formatted call and use them as java objects such as String or Integer ,in a rest client i would enter
{
"Name":"HelloWorld"
}
And i would get back the HelloWorld mapped to its Key so far ive seen examples but im just having trouble finding out what each tag does and how to parse the body to give the above results
#POST
#Path("/SetFeeds")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
#JsonCreator
public String setFeed(String jsonBody,#Context UriInfo uriInfo){
// Code to manipulate the body of the request
return response;
}
First thing you need to understand is how request body parsing is done. In JAX-RS parsing (or unmarshalling/deserializing/whatever) is done with MessageBodyReaders. There are different readers that can handle different Content-Type. For instance if you have Content-Type application/octet-stream, there is a reader that will unmarshal to byte[] or File or InputStream. So the following would work out the box
#Consumes("application/octet-stream")
public Response post(File file) {} // or `byte[]` or `InputStream`
That being said, JAX-RS implementations come with very basic readers for "easily convertible" format. For example, most requests can be converted to String, so you get that free for most Content-Types, as you are with your current code.
If we want some more complex data types, like your HelloWorld for Content-Type application/json, there is no standard reader for this. For this to work, we either need to create our own reader or use a library that comes with a reader. Luckily, the most popular JSON library in Java, Jackson, has implemented a JAX-RS provider that has a reader and a writer (for serialization).
Now depending on what server/JAX-RS implementation you are using, different implementations create light wrappers around the core Jackson JAX-RS module. If I knew the JAX-RS implementation you were using, I could recommend which wrapper to use, or you can forget the wrapper and just go with the basic Jackson module, which is
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.2.3</version>
</dependency>
The above is a Maven dependency. If you are not using Maven, then basically you need to download all these jars.
You can find all of them here. Just search for them individually.
Then you need to register the provider. Again it depends on your JAX-RS implementation and how you are handling the configuration of your resource classes. I would need to see your application configuration (either web.xml or Java code) and maybe the server you are using to help with that. For the most part, the JacksonJsonProvider (which is the reader and writer) needs to be registered.
Once you have it registered then you need to understand the basics of how Jackson handles the serialization. At most basic level, Jackson looks for JavaBean properties (basic getter/setter) to match with JSON properties. For instance, if you have this bean property
public class HelloWorld {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
The JSON should look like {"name": "whatever"}. The "name" key is the same as the bean property. In Bean property terms, the name of the property is all letters after the get/set with the first letter lowercased.
That's pretty much all there is to it. Now you can do
#Consumes("application/json")
public Response post(HelloWorld helloWorld) {
String name = helloWorld.getName(); // should == "whatever"
return Response.ok(helloWorld).build(); // we can also return objects
}
For more complex JSON formats, you should refer to the Jackson documentation or ask a question here on SO.
As far as the registering of the JacksonJsonProvider, if you are having trouble, please provide the information I requested, i.e. application configuration (web.xml or Java config) and the server you are using.
See Also:
JAX-RS Entity Provider to learn more about readers and writers
Level 3 RESTful API's feature custom media-types like application/vnd.service.entity.v1+json, for example. In my case I am using HAL to provide links between related resources in my JSON.
I'm not clear on the correct format for a custom media-type that uses HAL+JSON. What I have currently, looks like application/vnd.service.entity.v1.hal+json. I initially went with application/vnd.service.entity.v1+hal+json, but the +hal suffix is not registered and therefore violates section 4.2.8 of RFC6838.
Now Spring HATEOAS supports links in JSON out of the box but for HAL-JSON specifically, you need to use #EnableHypermediaSupport(type=EnableHypermediaSupport.HypermediaType.HAL). In my case, since I am using Spring Boot, I attach this to my initializer class (i.e., the one that extends SpringBootServletInitializer). But Spring Boot will not recognize my custom media-types out of the box. So for that, I had to figure out how to let it know that it needs to use the HAL object-mapper for media-types of the form application/vnd.service.entity.v1.hal+json.
For my first attempt, I added the following to my Spring Boot initializer:
#Bean
public HttpMessageConverters customConverters() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "json", Charset.defaultCharset()),
new MediaType("application", "*+json", Charset.defaultCharset()),
new MediaType("application", "hal+json"),
new MediaType("application", "*hal+json")
));
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
halObjectMapper.registerModule(new Jackson2HalModule());
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider));
converter.setObjectMapper(halObjectMapper);
return new HttpMessageConverters(converter);
}
This worked and I was getting the links back in proper HAL format. However, this was coincidental. This is because the actual media-type that ends up being reported as "compatible" with application/vnd.service.entity.v1.hal+json is *+json; it doesn't recognize it against application/*hal+json (see later for explanation). I didn't like this solution since it was polluting the existing JSON converter with HAL concerns. So, I made a different solution like so:
#Configuration
public class ApplicationConfiguration {
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
#Autowired
private BeanFactory beanFactory;
#Bean
public HttpMessageConverters customConverters() {
return new HttpMessageConverters(new HalMappingJackson2HttpMessageConverter());
}
private class HalMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
public HalMappingJackson2HttpMessageConverter() {
setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "hal+json"),
new MediaType("application", "*hal+json")
));
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
setObjectMapper(halObjectMapper);
}
}
}
This solution does not work; I end up getting links in my JSON that don't conform to HAL. This is because application/vnd.service.entity.v1.hal+json is not recognized by application/*hal+json. The reason this happens is that MimeType, which checks for media-type compatibility, only recognizes media-types that start with *+ as valid wild-card media-types for subtypes (e.g., application/*+json). This is why the first solution worked (coincidentally).
So there are two problems here:
MimeType will never recognize vendor-specific HAL media-types of the form application/vnd.service.entity.v1.hal+json against application/*hal+json.
MimeType will recognize vendor-specific HAL media-types of the form application/vnd.service.entity.v1+hal+json against application/*+hal+json, however these are invalid mimetypes as per section 4.2.8 of RFC6838.
It seems like the only right way would be if +hal is recognized as a valid suffix, in which case the second option above would be fine. Otherwise there is no way any other kind of wild-card media-type could specifically recognize vendor-specific HAL media-types. The only option would be to override the existing JSON message converter with HAL concerns (see first solution).
Another workaround for now would be to specify every custom media-type you are using, when creating the list of supported media-types for the message converter. That is:
#Configuration
public class ApplicationConfiguration {
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
#Autowired
private BeanFactory beanFactory;
#Bean
public HttpMessageConverters customConverters() {
return new HttpMessageConverters(new HalMappingJackson2HttpMessageConverter());
}
private class HalMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
public HalMappingJackson2HttpMessageConverter() {
setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "hal+json"),
new MediaType("application", "vnd.service.entity.v1.hal+json"),
new MediaType("application", "vnd.service.another-entity.v1.hal+json"),
new MediaType("application", "vnd.service.one-more-entity.v1.hal+json")
));
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
setObjectMapper(halObjectMapper);
}
}
}
This has the benefit of not polluting the existing JSON converter, but seems less than elegant. Does anyone know the right solution for this? Am I going about this completely wrong?
Although this question is a litte bit old, I recently stumbled upon the same problem so I wanted to give my 2 cents to this topic.
I think the problem here is the understanding of HAL regarding JSON. As you already pointed out here, all HAL is JSON but not all JSON is HAL. The difference between both is, from my understanding, that HAL defines some conventions to the semantics/structure, like telling you that behind an attribute like _links you'll find some links, whereas JSON just defines the format like key: [value] (as #zeroflagL already mentioned)
This is the reason, why the media type is called application/hal+json. It basically says it's the HAL style/semantics in the JSON format. This is also the reason that there exists a media type application/hal+xml (source ).
Now with a vendor specific media type, you define your own semantics and so your replacing the hal in application/hal+json and don't extend it.
If I understand you right, you basically want to say that you have a custom media type that uses the HAL style for it's JSON formatting. (This way, a client could use some HAL library to easily parse your JSON.)
So, at the end I think you basically have to decide wether you want to differentiate between JSON and HAL-based JSON and wether your API should provide one of these or both.
If you want to provide both, you'll have to define two different media types vnd.service.entity.v1.hal+json AND vnd.service.entity.v1+json. For the vnd.service.entity.v1.hal+json media type you then have to add your customized MappingJackson2HttpMessageConverter that uses the _halObjectMapper to return HAL-based JSON whereas the +json media type is supported by default returning your resource in good old JSON.
If you always want to provide HAL-based JSON, you have to enable HAL as the default JSON-Media type (for instance, by adding a customized MappingJackson2HttpMessageConverter that supports the +json media type and uses the _halObjectMapper mentioned before), so every request to application/vnd.service.entity.v1+json is handled by this converter returning HAL-based JSON.
From my opinion I think the right way is to only differentiate between JSON and other formats like XML and in your media type documentation you'd say, that your JSON is HAL-inspired in a way that clients can use HAL libs to parse the responses.
EDIT:
To bypass the problem that you'll have to add each vendor specific media type separately, you could override the isCompatibleWith method of the media type you're adding to your custom MappingJackson2HttpMessageConverter
converter.setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "doesntmatter") {
#Override
public boolean isCompatibleWith(final MediaType other) {
if (other == null) {
return false;
}
else if (other.getSubtype().startsWith("vnd.") && other.getSubtype().endsWith("+json")) {
return true;
}
return super.isCompatibleWith(other);
}
}
));