I'm working with a remote API which can change and I have no control over it.
This is why I have written myself a small jackson module that has a DeserializationProblemHandler implementation which logs those "unknown properties".
This works just fine, but I want to make my life easier and log the actual received JSON in the logs as well.
So having the following JSON example:
{ "myUnknownProperty": "value123", "myIgnoredProperty": true, "myKnownProperty": true }
A log should look something similar to this:
WARN: Found unexpected property while parsing json: /myUnknownProperty (type: com.foo.PartiallyIgnoredPOJO); JSON { "myUnknownProperty": "value123", "myIgnoredProperty": true, "myKnownProperty": true }
Here is the test case I currently have:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class ExampleTest
{
// Test
#Test
public void mockTest() throws JsonProcessingException
{
Logger logger = mock(Logger.class);
try
{
new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.registerModule(new UnknownPropertyModule(logger))
.readValue(partiallyIgnoredSONSample, PartiallyIgnoredPOJO.class);
}
finally
{
verify(logger).warn(UnknownPropertyModule.MESSAGE,
"/myUnknownProperty",
PartiallyIgnoredPOJO.class,
partiallyIgnoredSONSample);
}
}
// Implementations
private static class UnknownPropertyDeserializationProblemHandler extends DeserializationProblemHandler
{
private final Logger logger;
private final String format;
UnknownPropertyDeserializationProblemHandler(final Logger logger, final String format)
{
this.logger = logger;
this.format = format;
}
#Override
public boolean handleUnknownProperty(final DeserializationContext context,
final JsonParser parser,
final JsonDeserializer<?> deserializer,
final Object beanOrClass,
final String propertyName) throws IOException
{
final Class<?> type = beanOrClass.getClass();
String jsonPath = parser.getParsingContext().pathAsPointer().toString();
String json = "__How_to_get_the_entire_JSON_from_the_root___";
logger.warn(format, jsonPath, type, json);
return false;
}
}
private static class UnknownPropertyModule extends Module
{
public static final String MESSAGE = "Found unexpected property while parsing json: {} (type: {}); JSON: {}";
private final Logger logger;
public UnknownPropertyModule(#Nullable final Logger logger)
{
this.logger = logger != null ? logger : LoggerFactory.getLogger(getClass().getName());
}
#Override
public String getModuleName()
{
return getClass().getSimpleName();
}
#Override
public Version version()
{
return Version.unknownVersion();
}
#Override
public void setupModule(Module.SetupContext context)
{
context.addDeserializationProblemHandler(new UnknownPropertyDeserializationProblemHandler(logger, MESSAGE));
}
}
// Example json & mapping.
private static final String partiallyIgnoredSONSample = "{ \"myUnknownProperty\": \"value123\", \"myIgnoredProperty\": true, \"myKnownProperty\": true }";
#JsonIgnoreProperties("myIgnoredProperty")
private static class PartiallyIgnoredPOJO
{
private final boolean myKnownProperty;
#JsonCreator
public PartiallyIgnoredPOJO(#JsonProperty("myKnownProperty") boolean myKnownProperty)
{
this.myKnownProperty = myKnownProperty;
}
public boolean getMyKnownProperty()
{
return myKnownProperty;
}
}
}
From what I've seen in the debugger, you could use parser.getParsingContext().getParent() to get to the json root, but I'm unsure how to get the actual buffered data. Any ideas?
Good morning, I am new to Spring Boot, and I am performing a rest service that must invoke a procedure stored in the database, the question is that you receive the mobile and must return a code and result, as shown below:
This is my code:
Main Class
package com.app.validacion;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Controller
package com.app.validacion.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.app.validacion.dao.DriverBonificadosRepository;
import com.app.validacion.entity.RespuestaVo;
#RestController
public class DriverBonificadosController {
#Autowired
private DriverBonificadosRepository dao;
#GetMapping("/service/{movil}")
public RespuestaVo ConsultarMovil(#PathVariable String movil) {
return dao.validarClienteBonifiado(movil);
}
}
Repository
package com.app.validacion.dao;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.CrudRepository;
import com.app.validacion.entity.DriverBonificados;
import com.app.validacion.entity.RespuestaVo;
public interface DriverBonificadosRepository extends CrudRepository<DriverBonificados, Integer> {
#Procedure(procedureName="ValidacionClienteBonificado")
RespuestaVo validarClienteBonifiado(String pMovil);
}
My entity
import javax.persistence.NamedStoredProcedureQueries;
import javax.persistence.NamedStoredProcedureQuery;
import javax.persistence.ParameterMode;
import javax.persistence.StoredProcedureParameter;
import javax.persistence.Table;
#NamedStoredProcedureQueries({
#NamedStoredProcedureQuery(
name="SPValidationClienteBonus4G",
procedureName="ValidacionClienteBonificado",
parameters = {
#StoredProcedureParameter(mode=ParameterMode.IN, name="p_movil",type=String.class),
#StoredProcedureParameter(mode=ParameterMode.OUT, name="code",type=String.class),
#StoredProcedureParameter(mode=ParameterMode.OUT, name="result",type=String.class),
})
})
#Entity
#Table
public class DriverBonificados {
#Id
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getMovil() {
return movil;
}
public void setMovil(String movil) {
this.movil = movil;
}
public String getContador() {
return contador;
}
public void setContador(String contador) {
this.contador = contador;
}
public Date getFecha_driver() {
return fecha_driver;
}
public void setFecha_driver(Date fecha_driver) {
this.fecha_driver = fecha_driver;
}
public Date getFecha_alta() {
return fecha_alta;
}
public void setFecha_alta(Date fecha_alta) {
this.fecha_alta = fecha_alta;
}
public Date getFecha_fin() {
return fecha_fin;
}
public void setFecha_fin(Date fecha_fin) {
this.fecha_fin = fecha_fin;
}
public Date getCodigo_transaccion() {
return codigo_transaccion;
}
public void setCodigo_transaccion(Date codigo_transaccion) {
this.codigo_transaccion = codigo_transaccion;
}
private String movil;
private String contador;
private Date fecha_driver;
private Date fecha_alta;
private Date fecha_fin;
private Date codigo_transaccion;
My Class RespuestaVo
package com.app.validacion.entity;
public class RespuestaVo {
private String code;
private String result;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
And I get the following error (the mobile parameter must be received as a String, since in the database it is found as Varchar):
Anyone have an idea how this problem could be solved? I need to consult via Stored Procedue if or if
UPDATE
Using #Query and modifying the code as follows:
package com.app.validacion.dao;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import com.app.validacion.entity.DriverBonificados;
import com.app.validacion.entity.RespuestaVo;
public interface DriverBonificadosRepository extends CrudRepository<DriverBonificados, Integer> {
#Query(nativeQuery = true,value = "call ValidacionClienteBonificado(:movil)")
RespuestaVo validarClienteBonifiado(#Param("movil") String pMovil);
}
I get the following error:
org.springframework.core.convert.ConverterNotFoundException: No
converter found capable of converting from type
[org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap]
to type [com.app.validacion.entity.RespuestaVo] at
org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:321)
~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at
org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:194)
~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at
org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:174)
~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor$ProjectingConverter.convert(ResultProcessor.java:297)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor$ChainingConverter.lambda$and$0(ResultProcessor.java:217)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor$ChainingConverter.convert(ResultProcessor.java:228)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor.processResult(ResultProcessor.java:170)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:157)
~[spring-data-jpa-2.2.1.RELEASE.jar:2.2.1.RELEASE]
SOLVED
I managed to solve my problem, using the #Query annotation, and building an interface for the response I was going to receive, in these cases with 2 methods (according to the number of parameters that I will receive), and With this I got my answer in a Json, I leave the interface code below:
public interface RespuestaVo {
String getCode();
String getResult();
}
I recommend using #Query to run Stored Procedue with Spring Boot
Try this -
#GetMapping("/service/{movil}")
public RespuestaVo ConsultarMovil(#PathVariable("movil") String movil) {
return dao.validarClienteBonifiado(movil);
}
I have a model that I validate with #Valid in my Controllers when requests are send from the front-end:
#NotNull
#Size(min=1, message="Name should be at least 1 character.")
private String name;
#NotNull
#Pattern(regexp = "^https://github.com/.+/.+$", message = "Link to github should match https://github.com/USER/REPOSITORY")
private String github;
but now I am also creating a object with Jackson's ObjectMapper without the controller. Is there a way to register this validation in the ObjectMapper or should I just check the variables in the setters?
You can extend BeanDeserializer and validate object after deserialisation. To register this bean use SimpleModule.
Simple bean deserialiser with validation:
class BeanValidationDeserializer extends BeanDeserializer {
private final static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private final Validator validator = factory.getValidator();
public BeanValidationDeserializer(BeanDeserializerBase src) {
super(src);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Object instance = super.deserialize(p, ctxt);
validate(instance);
return instance;
}
private void validate(Object instance) {
Set<ConstraintViolation<Object>> violations = validator.validate(instance);
if (violations.size() > 0) {
StringBuilder msg = new StringBuilder();
msg.append("JSON object is not valid. Reasons (").append(violations.size()).append("): ");
for (ConstraintViolation<Object> violation : violations) {
msg.append(violation.getMessage()).append(", ");
}
throw new ConstraintViolationException(msg.toString(), violations);
}
}
}
We can use it as below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.File;
import java.io.IOException;
import java.util.Set;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule validationModule = new SimpleModule();
validationModule.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (deserializer instanceof BeanDeserializer) {
return new BeanValidationDeserializer((BeanDeserializer) deserializer);
}
return deserializer;
}
});
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(validationModule);
System.out.println(mapper.readValue(jsonFile, Pojo.class));
}
}
class Pojo {
#NotNull
#Size(min = 1, message = "Name should be at least 1 character.")
private String name;
#NotNull
#Pattern(regexp = "^https://github.com/.+/.+$", message = "Link to github should match https://github.com/USER/REPOSITORY")
private String github;
// getters, setters, toString()
}
For valid JSON payload:
{
"name": "Jackson",
"github": "https://github.com/FasterXML/jackson-databind"
}
prints:
Pojo{name='Jackson', github='https://github.com/FasterXML/jackson-databind'}
For invalid JSON payload:
{
"name": "",
"github": "https://git-hub.com/FasterXML/jackson-databind"
}
prints:
Exception in thread "main" javax.validation.ConstraintViolationException: JSON object is not valid. Reasons (2): Name should be at least 1 character., Link to github should match https://github.com/USER/REPOSITORY,
at BeanValidationDeserializer.validate(JsonApp.java:110)
at BeanValidationDeserializer.deserialize(JsonApp.java:97)
See also:
Java Bean Validation Basics
Deserialize to String or Object using Jackson
Jackson custom serialization and deserialization
I will also post how I managed to do it. Creating class that implements validator:
public class UserValidator implements Validator {
private static final int MINIMUM_NAME_LENGTH = 6;
#Override
public boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "Name must be at least 7 characters long.");
User foo = (User) target;
if(foo.getGithub().length() > 0 && !extensionSpec.getGithub().matches("^(www|http:|https:)+//github.com/.+/.+$")){
errors.rejectValue("github", "Github must match http://github.com/:user/:repo");
}
if (errors.getFieldErrorCount("name") == 0 && foo.getName().trim().length() < MINIMUM_NAME_LENGTH) {
errors.rejectValue("name", "Name must be at least 7 characters");
}
}
}
Then creating databinder with the deserialized object, taking the binding result and then validating the object:
ObjectMapper mapper = new ObjectMapper();
User foo = mapper.readValue(FooJson, User.class);
Validator validator = new ObjectValidator();
BindingResult bindingResult = new DataBinder(foo).getBindingResult();
validator.validate(foo, bindingResult);
if(bindingResult.hasErrors()){
throw new BindException(bindingResult);
}
Also if you want to take the errorCodes in the body of the response:
#ExceptionHandler
ResponseEntity handleBindException(BindException e){
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getBindingResult().getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getCode)
.toArray());
}
I am using Jackson XML mapper to deserialize XML to POJO. The XML looks like
<person>
<agency>
<phone>111-111-1111</phone>
</agency>
</person>
And my class looks like
class Person
{
#JacksonXmlProperty(localName="agency", namespace="namespace")
private Agency agency;
//getter and setter
}
class Agency
{
#JacksonXmlElementWrapper(useWrapping = false)
#JacksonXmlProperty(localName="phone", namespace="namespace")
private List<AgencyPhone> phones;
//getter and setter
}
class AgencyPhone
{
private Phone phone;
//getter and setter
}
class Phone
{
private String number;
//getter and setter
}
I want to set the phone number to number in Phone class. I cannot change XML or the way the class has been structured. I am getting Cannot construct instance of resolved.agency.AgencyPhone error and I created a AgencyPhone constructor
class AgencyPhone{
{
private Phone phone;
public AgencyPhone(Phone phone)
{
this.phone = phone;
}
}
But that did not work. So how to deserialize to nested instances.
You can write your own custom deserialiser to achieve this. Here is the code to get you started:
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
XmlMapper mapper = new XmlMapper();
final SimpleModule module = new SimpleModule("configModule", com.fasterxml.jackson.core.Version.unknownVersion());
module.addDeserializer(Person.class, new DeSerializer());
mapper.registerModule(module);
// Person readValue = mapper.readValue(<xml source>);
}
}
class DeSerializer extends StdDeserializer<Person> {
protected DeSerializer() {
super(Person.class);
}
#Override
public Person deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// use p.getText() and p.nextToken to navigate through the xml and construct Person object
return new Person();
}
}
Since Jersey 2.9, it's been possible to create link relations for hypermedia-driven REST APIs through declarative linking.
This code, for example:
#InjectLink(
resource = ItemResource.class,
style = Style.ABSOLUTE,
bindings = #Binding(name = "id", value = "${instance.id}"),
rel = "self"
)
#XmlJavaTypeAdapter(Link.JaxbAdapter.class)
#XmlElement(name="link")
Link self;
...in theory is expected to produce JSON like this:
"link" : {
"rel" : "self",
"href" : "http://localhost/api/resource/1"
}
However, Jersey produces different JSON with a lot of properties that I don't need:
"link" : {
"rel" : "self",
"uri" : "http://localhost/api/resource/1",
"type": null,
"uriBuilder" : null
}
Notice also that instead of href, it uses uri. I looked at Jersey's implementation of the Link object and found JerseyLink.
I want to use Jersey's declarative linking instead of rolling out my own implementation. I ended up using Jackson annotations just to ignore other JerseyLink properties.
#JsonIgnoreProperties({ "uriBuilder", "params", "type", "rels" })
Has anyone used declarative linking with Jersey and had the expected JSON output (e.g., href instead of uri, without extra Jersey properties) without having to use JsonIgnoreProperties or other hacks?
Thanks.
EDIT
I resolved this using an approach which I think is a hack but works well for me and doesn't require the use of a complicated adapter.
I realized that I can actually expose a different object instead of the Link injected by Jersey.
I created a wrapper object named ResourceLink:
public class ResourceLink {
private String rel;
private URI href;
//getters and setters
}
Then in my representation object I have a getter method:
public ResourceLink getLink() {
ResourceLink link = new ResourceLink();
link.setRel(self.getRel());
link.setHref(self.getUri());
return link;
}
So I used Jersey to inject the link but returned a different object in a getter method in my representation object. This would be the property that would be serialized to JSON and not the injected link object because I didn't create a getter method for it.
Invesitigation
Environment: Jersey 2.13 ( all provider versions are also 2.13 ).
Whether you use declarative or programmatic linking, the serialization shouldn't differ. I chose programmatic, just because I can :-)
Test classes:
#XmlRootElement
public class TestClass {
private javax.ws.rs.core.Link link;
public void setLink(Link link) { this.link = link; }
#XmlElement(name = "link")
#XmlJavaTypeAdapter(Link.JaxbAdapter.class)
public Link getLink() { return link; }
}
#Path("/links")
public class LinkResource {
#GET
#Produces(MediaType.APPLICATION_JSON)
public Response getResponse() {
URI uri = URI.create("https://stackoverflow.com/questions/24968448");
Link link = Link.fromUri(uri).rel("stackoverflow").build();
TestClass test = new TestClass();
test.setLink(link);
return Response.ok(test).build();
}
}
#Test
public void testGetIt() {
WebTarget baseTarget = target.path("links");
String json = baseTarget.request().accept(
MediaType.APPLICATION_JSON).get(String.class);
System.out.println(json);
}
Results with different Providers (with no extra configurations)
jersey-media-moxy
Dependency
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-moxy</artifactId>
</dependency>
Result (weird)
{
"link": "javax.ws.rs.core.Link$JaxbLink#cce17d1b"
}
jersey-media-json-jackson
Dependency
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
</dependency>
Result (close, but what's with the params?)
{
"link": {
"params": {
"rel": "stackoverflow"
},
"href": "https://stackoverflow.com/questions/24968448"
}
}
jackson-jaxrs-json-provider
Dependency
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.4.0</version>
</dependency>
Result (Two different results, with two different JSON providers)
resourceConfig.register(JacksonJsonProvider.class);
{
"link": {
"uri": "https://stackoverflow.com/questions/24968448",
"params": {
"rel": "stackoverflow"
},
"type": null,
"uriBuilder": {
"absolute": true
},
"rels": ["stackoverflow"],
"title": null,
"rel": "stackoverflow"
}
}
resourceConfig.register(JacksonJaxbJsonProvider.class);
{
"link": {
"params": {
"rel": "stackoverflow"
},
"href": "https://stackoverflow.com/questions/24968448"
}
}
My Conclusions
We are annotating the field with #XmlJavaTypeAdapter(Link.JaxbAdapter.class). Let look at a snippet of this adapter
public static class JaxbAdapter extends XmlAdapter<JaxbLink, Link> {...}
So from Link, we are being marshalled to JaxbLink
public static class JaxbLink {
private URI uri;
private Map<QName, Object> params;
...
}
jersey-media-moxy
Seems to be a bug... See below in solutions.
The others
The other two are dependent on jackson-module-jaxb-annotations to handle marshalling using JAXB annotations. jersey-media-json-jackson will automatically register the required JaxbAnnotationModule. For jackson-jaxrs-json-provider, using JacksonJsonProvider will not support JAXB annotations (without confgiruation), and using JacksonJsonJaxbProvider will give us the JAXB annotation support.
So if we have JAXB annotation support, we will get marshalled to JaxbLink, which will give this result
{
"link": {
"params": {
"rel": "stackoverflow"
},
"href": "https://stackoverflow.com/questions/24968448"
}
}
The ways we can get the result with all the unwanted properties, is to 1), use the jackson-jaxrs-json-provider's JacksonJsonProvider or 2), create a ContextResolver for ObjectMapper where we don't register the JaxbAnnotationModule. You seem to be doing one of those.
Solutions
The above still doesn't get us where we want to get to (i.e. no params).
For jersey-media-json-jackson and jackson-jaxrs-json-provider...
...which use Jackson, the only thing I can think of at this point is to create a custom serializer
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import javax.ws.rs.core.Link;
public class LinkSerializer extends JsonSerializer<Link>{
#Override
public void serialize(Link link, JsonGenerator jg, SerializerProvider sp)
throws IOException, JsonProcessingException {
jg.writeStartObject();
jg.writeStringField("rel", link.getRel());
jg.writeStringField("href", link.getUri().toString());
jg.writeEndObject();
}
}
Then create a ContextResolver for the ObjectMapper, where we register the serializer
#Provider
public class ObjectMapperContextResolver
implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public ObjectMapperContextResolver() {
mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Link.class, new LinkSerializer());
mapper.registerModule(simpleModule);
}
#Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
}
This is the result
{
"link": {
"rel": "stackoverflow",
"href": "https://stackoverflow.com/questions/24968448"
}
}
With jersey-media-moxy, it appears there's a Bug with missing setters in the JaxbLink class, so the marshalling reverts to calling toString, which is what's shown above. A work around, as proposed here by Garard Davidson, is just to create another adapter
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.Link;
import javax.xml.bind.annotation.XmlAnyAttribute;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.namespace.QName;
public class LinkAdapter
extends XmlAdapter<LinkJaxb, Link> {
public LinkAdapter() {
}
public Link unmarshal(LinkJaxb p1) {
Link.Builder builder = Link.fromUri(p1.getUri());
for (Map.Entry<QName, Object> entry : p1.getParams().entrySet()) {
builder.param(entry.getKey().getLocalPart(), entry.getValue().toString());
}
return builder.build();
}
public LinkJaxb marshal(Link p1) {
Map<QName, Object> params = new HashMap<>();
for (Map.Entry<String,String> entry : p1.getParams().entrySet()) {
params.put(new QName("", entry.getKey()), entry.getValue());
}
return new LinkJaxb(p1.getUri(), params);
}
}
class LinkJaxb {
private URI uri;
private Map<QName, Object> params;
public LinkJaxb() {
this (null, null);
}
public LinkJaxb(URI uri) {
this(uri, null);
}
public LinkJaxb(URI uri, Map<QName, Object> map) {
this.uri = uri;
this.params = map!=null ? map : new HashMap<QName, Object>();
}
#XmlAttribute(name = "href")
public URI getUri() {
return uri;
}
#XmlAnyAttribute
public Map<QName, Object> getParams() {
return params;
}
public void setUri(URI uri) {
this.uri = uri;
}
public void setParams(Map<QName, Object> params) {
this.params = params;
}
}
Using this adapter instead
#XmlElement(name = "link")
#XmlJavaTypeAdapter(LinkAdapter.class)
private Link link;
will give us the desired output
{
"link": {
"href": "https://stackoverflow.com/questions/24968448",
"rel": "stackoverflow"
}
}
UPDATE
Now that I think about it, the LinkAdapter would work with the Jackson provider also. No need to create a Jackson Serializer/Deserializer. The Jackson module should already support the JAXB annotations out the box, given the JacksonFeature is enabled. The examples above show using the JAXB/JSON providers separately, but given just the JacksonFeature is enabled, the JAXB version of the provider should be used. This may actually be the more preferred solution. No need to create an ContextResolvers for the ObjectMapper :-D
It's also possible to declare the annotation at the package level, as seen here
I'd like to share with my solution for serialising/deserialising Link objects using with Jackson and the mix-in annotations.
LinkMixin:
#JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.NONE,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
#JsonDeserialize(using = LinkMixin.LinkDeserializer.class)
public abstract class LinkMixin extends Link {
private static final String HREF = "href";
#JsonProperty(HREF)
#Override
public abstract URI getUri();
#JsonAnyGetter
public abstract Map<String, String> getParams();
public static class LinkDeserializer extends JsonDeserializer<Link> {
#Override
public Link deserialize(
final JsonParser p,
final DeserializationContext ctxt) throws IOException {
final Map<String, String> params = p.readValueAs(
new TypeReference<Map<String, String>>() {});
if (params == null) {
return null;
}
final String uri = params.remove(HREF);
if (uri == null) {
return null;
}
final Builder builder = Link.fromUri(uri);
params.forEach(builder::param);
return builder.build();
}
}
}
Example:
final ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Link.class, LinkMixin.class);
final Link link = Link.fromUri("http://example.com")
.rel("self")
.title("xxx")
.param("custom", "my")
.build();
final String json = mapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(Collections.singleton(link));
System.out.println(json);
final List<Link> o = mapper.readValue(json, new TypeReference<List<Link>>() {});
System.out.println(o);
Output:
[ {
"href" : "http://example.com",
"rel" : "self",
"title" : "xxx",
"custom" : "my"
} ]
[<http://example.com>; rel="self"; title="xxx"; custom="my"]
Using the suggested update solution I was still getting the rel part inside the params map.
I have made some changes in the Link adapter class
public class LinkAdapter
extends XmlAdapter<LinkJaxb, Link> {
public LinkAdapter() {
}
public Link unmarshal(LinkJaxb p1) {
Link.Builder builder = Link.fromUri(p1.getUri());
return builder.build();
}
public LinkJaxb marshal(Link p1) {
return new LinkJaxb(p1.getUri(), p1.getRel());
}
}
class LinkJaxb {
private URI uri;
private String rel;
public LinkJaxb() {
this (null, null);
}
public LinkJaxb(URI uri) {
this(uri, null);
}
public LinkJaxb(URI uri,String rel) {
this.uri = uri;
this.rel = rel;
}
#XmlAttribute(name = "href")
public URI getUri() {
return uri;
}
#XmlAttribute(name = "rel")
public String getRel(){return rel;}
public void setUri(URI uri) {
this.uri = uri;
}
}
It now holds only the two params which are needed (rel,href)
I did not really understand when do I need to unmarshal a Jaxb link to a Link.
What mattered to me was the Link to Jaxb link marshaling.
Thank You, #peeskillet and #Nir Sivan, for your answers. But I was able to make it work without using the LinkAdapter or ContextResolver<ObjectMapper>.
I just added a instance variable of the custom Link type (here ResourceLink which is analogous to your LinkJaxb) to my entity class as a #Transient property and after that Jackson configuration automatically included that attribute in the Response JSON
Resource Link - Class
import javax.xml.bind.annotation.XmlAttribute;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
#JsonInclude(Include.NON_EMPTY)
public class ResourceLink {
private String uri;
private String rel;
public ResourceLink() {
this (null, null);
}
public ResourceLink(String uri) {
this(uri, null);
}
public ResourceLink(String uri,String rel) {
this.uri = uri;
this.rel = rel;
}
#XmlAttribute(name = "href")
public String getUri() {
return uri;
}
#XmlAttribute(name = "rel")
public String getRel(){return rel;}
public void setUri(String uri) {
this.uri = uri;
}
}
Entity Class
package com.bts.entities;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import com.bts.rs.root.util.ResourceLink;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
#Entity
#Table(name="cities")
#JsonInclude(Include.NON_EMPTY)
public class City {
#Id
#Column(name="city_id")
private Integer cityId;
#Column(name="name")
private String name;
#Column(name="status")
private int status;
#Column(name="del_status")
private int delStatus;
#Transient
List<ResourceLink> links = new ArrayList<ResourceLink>();
// private
public City () {
}
public City (Integer id, String name) {
this.cityId = id;
this.name = name;
this.status = 0;
this.delStatus = 0;
}
// getters and setters for Non-transient properties
// Below is the getter for lInks transient attribute
public List<ResourceLink> getLinks(){
return this.links;
}
// a method to add links - need not be a setter necessarily
public void addResourceLink (String uri,String rel) {
this.links.add(new ResourceLink(uri, rel));
}
}
Jersy Resource Provider
#GET
#Path("/karanchadha")
#Produces({MediaType.APPLICATION_JSON})
#Transactional
public Response savePayment() {
City c1 = new City();
c1.setCityId(11);
c1.setName("Jamshedpur");
c1.addResourceLink("http://www.test.com/home", "self");
c1.addResourceLink("http://www.test.com/2", "parent");
return Response.status(201).entity(c1).build();
}
First dependencies :
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.34</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-declarative-linking</artifactId>
<version>2.34</version>
</dependency>
Second - config class:
package app.rest.config;
import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import org.glassfish.jersey.linking.DeclarativeLinkingFeature;
import org.glassfish.jersey.server.ResourceConfig;
#ApplicationPath(value = "rest")
public class RestApplicationConfig extends ResourceConfig{
public RestApplicationConfig() {
register(JacksonJaxbJsonProvider.class);
register(DeclarativeLinkingFeature.class);
packages("app.rest.controllers");
}
}
Third - create adapter class
package app.rest.config;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.bind.adapter.JsonbAdapter;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.UriBuilder;
public class CustomJsonAdapter implements JsonbAdapter<Link, JsonObject> {
#Override
public JsonObject adaptToJson(Link link) throws Exception {
StringBuilder builder = new StringBuilder();
builder.append("http://");
builder.append(link.getUri().getHost());
builder.append(":");
builder.append(link.getUri().getPort());
builder.append(link.getUri().getRawPath());
return Json.createObjectBuilder().add("href", builder.toString()).add("rel", link.getRel()).build();
}
#Override
public Link adaptFromJson(JsonObject json) throws Exception {
Link link = Link.fromUriBuilder(UriBuilder.fromUri(json.getString("href"))).rel(json.getString("rel")).build();
return link;
}
}
Forth - register JsonbConfig with custom adapter class from Link class to Json
package app.rest.config;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
#Provider
public class AppJsonConfig implements ContextResolver<Jsonb> {
#Override
public Jsonb getContext(Class<?> type) {
JsonbConfig jsonbConfig = new JsonbConfig();
jsonbConfig.withAdapters(new CustomJsonAdapter());
return JsonbBuilder.create(jsonbConfig);
}
}
Fifth : create the model with Link and #InjectLink annotation
package app.rest.model;
import java.io.Serializable;
import java.util.Objects;
import javax.json.bind.annotation.JsonbTypeAdapter;
import javax.ws.rs.core.Link;
import javax.xml.bind.annotation.XmlRootElement;
import org.glassfish.jersey.linking.Binding;
import org.glassfish.jersey.linking.InjectLink;
import org.glassfish.jersey.linking.InjectLink.Style;
import app.rest.config.CustomJsonAdapter;
import app.rest.controllers.RestController;
#XmlRootElement
public class StudentResource implements Serializable{
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String surname;
#InjectLink(resource = RestController.class,
style = Style.ABSOLUTE,
rel = "self",
method = "getStudentById",
bindings = #Binding(name = "id", value = "${instance.id}"))
#JsonbTypeAdapter(value = CustomJsonAdapter.class)
private Link link;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Link getLink() {
return link;
}
public void setLink(Link link) {
this.link = link;
}
#Override
public int hashCode() {
return Objects.hash(name, surname);
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
StudentResource other = (StudentResource) obj;
return Objects.equals(name, other.name) && Objects.equals(surname, other.surname);
}
}
And at the end rest controller
package app.rest.controllers;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import app.rest.model.*;
#Path(value = "student")
public class RestController {
#Context
private UriInfo uriInfo;
private final ArrayList<StudentResource> students = new ArrayList<>();
public RestController() {
StudentResource s1 = new StudentResource();
s1.setId(1L);
s1.setName("test1");
s1.setSurname("surTest1");
students.add(s1);
StudentResource s2 = new StudentResource();
s2.setId(2L);
s2.setName("new_St");
s2.setSurname("surNew");
students.add(s2);
}
#GET
#Produces(MediaType.APPLICATION_JSON)
public Response getAllStudents() throws URISyntaxException{
Link link = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()).rel("self").type("GET").build();
GenericEntity<List<StudentResource>> studentsEntities = new GenericEntity<List<StudentResource>>(students) {};
return Response.status(Response.Status.OK).entity(studentsEntities).links(link).build();
}
#GET
#Path(value = "/{id}")
#Produces(MediaType.APPLICATION_JSON)
public Response getStudentById(#PathParam(value = "id") Long id ) {
Iterator<StudentResource> iterator = students.iterator();
StudentResource studentById= null;
while (iterator.hasNext()) {
StudentResource next = iterator.next();
if(next.getId().equals(id)) {
studentById = next;
break;
}
}
if(null!=studentById) return Response.status(Response.Status.OK).entity(studentById).build();
else return Response.status(Response.Status.NO_CONTENT).build();
}
}
Deployed and tested on Payara 5.201
Produces : get http://localhost:8080/sampleappee/rest/student/1
{"id":1,"link":{"href":"http://localhost:8080/sampleappee/rest/student/1","rel":"self"},"name":"test1","surname":"surTest1"}