I'm trying to map directly the output of a Jsonpath to a list of POJO.
I'm using Jackson as a mapping provider.
Jsonpath output:
{
"actions" : [
{
"parameterDefinitions" : [
{
"defaultParameterValue" : {
"name" : "PARAM1",
"value" : ""
},
"description" : "Type String",
"name" : "PARAM1",
"type" : "StringParameterDefinition"
},
{
"defaultParameterValue" : {
"name" : "PARAM3",
"value" : ""
},
"description" : "Type String",
"name" : "PARAM3",
"type" : "StringParameterDefinition"
}
]
}
]
}
JobParameter.java (the POJO in which I'd like to map):
public class JobParameter {
private String description;
private String name;
private String type;
// public getters & setters
Jsonpath initialization:
Configuration conf = Configuration
.builder()
.mappingProvider(new JacksonMappingProvider())
.build();
List<JobParameter> jobParameters = JsonPath
.using(conf)
.parse(jsonpathOutput)
.read("$.actions[0].parameterDefinitions[0:]", List.class);
Using the above code, I always get a Map. See below the result of a toString() on this map:
[{defaultParameterValue={name=PARAM1, value=}, description=Type String, name=PARAM1, type=StringParameterDefinition}, {defaultParameterValue={name=PARAM3, value=}, description=Type String, name=PARAM3, type=StringParameterDefinition}]
Note that when I try to map the Jsonpath output to a single object, the deserialization works fine:
Configuration conf = Configuration
.builder()
.mappingProvider(new JacksonMappingProvider())
.build();
JobParameter singleJobParameter = JsonPath
.using(conf)
.parse(jsonpathOutput)
.read("$.actions[0].parameterDefinitions[0]", JobParameter .class);
In the example above, the singleJobParameter instance is well created and filled.
Am I missing something?
Thanks!
You must use a TypeRef for this. In your case you must also use the #JsonIgnoreProperties annotation.
Configuration conf = Configuration
.builder()
.mappingProvider(new JacksonMappingProvider())
.jsonProvider(new JacksonJsonProvider())
.build();
TypeRef<List<JobParameter>> type = new TypeRef<List<JobParameter>>(){};
List<JobParameter> jobParameters = JsonPath
.using(conf)
.parse(json)
.read("$.actions[0].parameterDefinitions[0:]", type);
Note that this is not supported by all JsonMappingProviders.
<dependency>
<groupId>com.github.jsurfer</groupId>
<artifactId>jsurfer-simple</artifactId>
<version>1.2.2</version>
</dependency>
With JsonSurfer, you can achieve this by simply two lines:
JsonSurfer jsonSurfer = JsonSurfer.jackson();
Collection<JobParameter> parameters = jsonSurfer.collectAll(json, JobParameter.class, "$.actions[0].parameterDefinitions[*]");
And don't forget to ignore the unused "defaultParameterValue" in your POJO.
#JsonIgnoreProperties({"defaultParameterValue"})
private static class JobParameter {
private String description;
private String name;
private String type;
Using a wrapper POJO solved the problem.
#JsonIgnoreProperties(ignoreUnknown = true)
public class JobParametersWrapper {
private List<JobParameter> parameterDefinitions;
public List<JobParameter> getParameterDefinitions() {
return parameterDefinitions;
}
public void setParameterDefinitions(List<JobParameter> parameterDefinitions) {
this.parameterDefinitions = parameterDefinitions;
}
}
Related
POJOs:
import lombok.Data;
#Data
public class CCMTRequest {
private MOEH cch;
private String filler1;
private CCMTCCD ccd;
private String uPwName;
}
#Data
public class MOEH {
private String c;
private int z;
private String dType;
}
#Data
public class CCMTCCD {
private dTime time;
private int x;
}
#Data
public class dTime {
private String dTime;
}
Test Class:
public class TestJacksonParser {
#Test
void load_jsonToPOJO() {
ObjectMapper mapper = new ObjectMapper();
ClassLoader load = this.getClass().getClassLoader();
File file = new File(load.getResource("request.json").getFile());
CCMTRequest req = null;
try {
req = mapper.readValue(file, CCMTRequest.class);
}
catch(Exception e) {
System.out.println(e.getMessage());
}
System.out.println("\nRequest: " + req);
}
}
request.json :
{
"cch" : {
"c" : "C",
"z" : 4678,
"dType" : "dtype"
},
"filler1" : "random filler1",
"ccd" : {
"time" : {
"dTime" : "4:35"
},
"x" : 34567
},
"uPwName" : "uPwName"
}
Error:
Unrecognized field "dType" (class com.spring.mapstruct.test.MOEH), not
marked as ignorable (3 known properties: "z", "c", "dtype"]) at
[Source: (File); line: 5, column: 14] (through reference chain:
com.spring.mapstruct.test.CCMTRequest["cch"]->com.spring.mapstruct.test.MOEH["dType"])
Request: null
Now, when I update my test class as :
public class TestJacksonParser {
#Test
void load_jsonToPOJO() {
ObjectMapper mapper = new ObjectMapper();
//ignore Unknown JSON Fields
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
ClassLoader load = this.getClass().getClassLoader();
File file = new File(load.getResource("request.json").getFile());
CCMTRequest req = null;
try {
req = mapper.readValue(file, CCMTRequest.class);
}
catch(Exception e) {
System.out.println(e.getMessage());
}
System.out.println("\nRequest: " + req);
}
}
I get output as:
Request: CCMTRequest(cch=MOEH(c=C, z=4678, dType=null), filler1=random
filler1, ccd=CCMTCCD(time=dTime(dTime=4:35), x=34567), uPwName=null)
So how jackson is working here with lombok, is there an issue with properties "dType" and "uPwName" ?
first things first, next time please provide better example rather than random name properties. it's confusing.
your problem is because lombok generate getter and setter for property like "uPwName" becomes "getUPwName()" and "setUPwName()". jackson read it as "getuPwName" and "setuPwName";
the library both using different naming convention for getters and setters.
there are 2 approach to fix this:
for your quick fix:
ObjectMapper mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
for better way to fix your problem: use better name for your properties.
Object Model:
public class NotificationSettingsDto {
private Boolean campaignEvents;
private Boolean drawResultEvents;
private Boolean transactionEvents;
private Boolean userWonEvents;
}
Say I'm getting this object
new NotificationSettingsDto(true,true,true,true);
through spring get request.
And this is the JSON value I want to get from this object.
[{"name" : "campaignEvents" , "value" : true},
{"name" : "drawResultEvents" , "value" : true},
{"name" : "transactionEvents" , "value" : true},
{"name" : "userWonEvents", "value" : true}]
This solved it :
Arrays.asList( new CustomPair<>("campaignEvents", nsDto.getCampaignEvents()),
new CustomPair<>("drawResults", nsDto.getDrawResultEvents()),
new CustomPair<>("transactionEvents", nsDto.getTransactionEvents()),
new CustomPair<>("userWonEvents", nsDto.getUserWonEvents())
nsDto stands for NotificationSettingsDto. Whereas CustomPair is:
public class CustomPair<K, V> {
private K key;
private V value;
}
#nafas was right in the comment section. Thanks. It's not the cleanest Solution but it does it
Resulted JSON :
[{"key":"campaignEvents","value":true},
{"key":"drawResults","value":true},
{"key":"transactionEvents","value":true},
{"key":"userWonEvents","value":true}]
You can use Jackson 2.x ObjectMapper class.
NotificationSettingsDto obj = new NotificationSettingsDto(true,true,true,true);
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(obj);
But your json string is not valid. This is how your json would look like:
{
"campaignEvents": true,
"drawResultEvents": true,
"transactionEvents": true,
"userWonEvents": true
}
EDIT: You can also do it using Gson as mentioned in the comment.
Gson gson = new Gson();
NotificationSettingsDto obj = new NotificationSettingsDto(true,true,true,true);
String jsonString = gson.toJson(obj);
I want to generate a swagger from a JAX-RS endpoint with an external enumeration definition however the generated swagger directly includes the enumeration into the definition of the model. It implies that the enumeration documentation is not generated but also that the same enumeration is duplicated on the client side.
I use the swagger-jaxrs dependency to scan my endpoint and generate the swagger json file. This GitHub repository can be used to reproduce the problem. I also have created a GitHub issue on the swagger-core repository.
The JAX-RS endpoint
#Api("hello")
#Path("/helloSwagger")
public class HelloSwagger {
#ApiOperation(value = "Get all unique customers", notes = "Get all customers matching the given search string.", responseContainer = "Set", response = User.class)
#GET
#Path("/getUniqueUsers")
#Produces(MediaType.APPLICATION_JSON)
public Set<User> getUniqueUsers(
#ApiParam(value = "The search string is used to find customer by their name. Not case sensitive.") #QueryParam("search") String searchString,
#ApiParam(value = "Limits the size of the result set", defaultValue = "50") #QueryParam("limit") int limit
) {
return new HashSet<>(Arrays.asList(new User(), new User()));
}
}
The model with the enumeration
public class User {
private String name = "unknown";
private SynchronizationStatus ldap1 = SynchronizationStatus.UNKNOWN;
private SynchronizationStatus ldap2 = SynchronizationStatus.OFFLINE;
#ApiModelProperty(value = "The user name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#ApiModelProperty(value = "The synchronization status with the LDAP1")
public SynchronizationStatus getLdap1() {
return ldap1;
}
public void setLdap1(SynchronizationStatus ldap1) {
this.ldap1 = ldap1;
}
public SynchronizationStatus getLdap2() {
return ldap2;
}
public void setLdap2(SynchronizationStatus ldap2) {
this.ldap2 = ldap2;
}
}
#ApiModel("The synchronization status with LDAP instance.")
public enum SynchronizationStatus {
UNKNOWN,
SYNC,
OFFLINE,
CONFLICT
}
An extract of the swagger generated
{
(...)
},
"definitions" : {
"User" : {
"type" : "object",
"properties" : {
"name" : {
"type" : "string",
"description" : "The user name"
},
"ldap1" : {
"type" : "string",
"description" : "The synchronization status with the LDAP1",
"enum" : [ "UNKNOWN", "SYNC", "OFFLINE", "CONFLICT" ]
},
"ldap2" : {
"type" : "string",
"enum" : [ "UNKNOWN", "SYNC", "OFFLINE", "CONFLICT" ]
}
}
}
}
}
Expected result
{
(...)
"definitions" : {
"SynchronizationStatus" : {
"description" : "The synchronization status with LDAP instance.",
"enum" : [ "UNKNOWN", "SYNC", "OFFLINE", "CONFLICT" ],
"type" : "string"
},
"User" : {
"type" : "object",
"properties" : {
"name" : {
"type" : "string",
"description" : "The user name"
},
"ldap1" : {
"$ref" : "#/definitions/SynchronizationStatus"
},
"ldap2" : {
"$ref" : "#/definitions/SynchronizationStatus"
}
}
}
}
}
Am I doing something wrong or is it a 'feature' of the swagger-jaxrs library ?
Thanks for your help
Am I doing something wrong or is it a 'feature' of the swagger-jaxrs
library ?
Enum value are treat as primitive value type by swagger and swagger out-of-the-box does not generate model definition for enum type (see code line 209 under). So the is a feature and not related with swagger-jaxrs.
However, you can generate the swagger definition, as per your expectation, by providing the custom model converter(io.swagger.converter.ModelConverter).
But it seems to me a nice feature to be available in swagger out-of-the-box.
Following is a ruff implementation which can help you to generate the expected swagger definition.
package nhenneaux.test.swagger.ext;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;
import com.fasterxml.jackson.databind.JavaType;
import io.swagger.annotations.ApiModel;
import io.swagger.converter.ModelConverter;
import io.swagger.converter.ModelConverterContext;
import io.swagger.jackson.ModelResolver;
import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.RefProperty;
import io.swagger.models.properties.StringProperty;
import io.swagger.util.Json;
public class EnumAsModelAwareResolver extends ModelResolver {
static final EnumAsModelAwareResolver INSTANCE = new EnumAsModelAwareResolver();
public EnumAsModelAwareResolver() {
super(Json.mapper());
}
#Override
public Property resolveProperty(Type type, ModelConverterContext context, Annotation[] annotations,
Iterator<ModelConverter> chain) {
if (isEnumAnApiModel(type)) {
String name = findName(type);
// ask context to resolver enum type (for adding model definition
// for enum under definitions section
context.resolve(type);
return new RefProperty(name);
}
return chain.next().resolveProperty(type, context, annotations, chain);
}
private String findName(Type type) {
JavaType javaType = _mapper.constructType(type);
Class<?> rawClass = javaType.getRawClass();
ApiModel annotation = rawClass.getAnnotation(ApiModel.class);
String name = annotation.value();
if (name == null || name.length() == 0) {
name = rawClass.getSimpleName();
}
return name;
}
private boolean isEnumAnApiModel(Type type) {
JavaType javaType = _mapper.constructType(type);
return javaType.isEnumType()
&& javaType.getRawClass().isAnnotationPresent(ApiModel.class);
}
#Override
public Model resolve(Type type, ModelConverterContext context, Iterator<ModelConverter> chain) {
JavaType javaType = Json.mapper().constructType(type);
if (javaType.isEnumType()) {
ModelImpl model = new ModelImpl();
Class<?> rawClass = javaType.getRawClass();
ApiModel annotation = rawClass.getAnnotation(ApiModel.class);
String name = annotation.value();
if (name == null || name.length() == 0) {
name = rawClass.getSimpleName();
}
model.setName(name);
model.setDescription(annotation.description());
model.setType(StringProperty.TYPE);
List<String> constants = findEnumConstants(rawClass);
model.setEnum(constants);
return model;
}
return chain.next().resolve(type, context, chain);
}
private List<String> findEnumConstants(Class<?> rawClass) {
StringProperty p = new StringProperty();
_addEnumProps(rawClass, p);
return p.getEnum();
}
}
package nhenneaux.test.swagger.ext;
import io.swagger.converter.ModelConverters;
import io.swagger.jaxrs.config.BeanConfig;
import nhenneaux.test.swagger.ext.EnumAsModelAwareResolver;
public class EnumModelAwareBeanConfig extends BeanConfig {
public EnumModelAwareBeanConfig() {
registerResolver();
}
private void registerResolver() {
ModelConverters modelConverters = ModelConverters.getInstance();
// remove and add; in case it is called multiple times.
// should find a better way to register this.
modelConverters.removeConverter(EnumAsModelAwareResolver.INSTANCE);
modelConverters.addConverter(EnumAsModelAwareResolver.INSTANCE);
}
}
In your test use:
final BeanConfig beanConfig = new nhenneaux.test.endpoint.model.EnumModelAwareBeanConfig();
Hops this helps.
You could try the reference attribute of the #ApiModelProperty annotation:
#ApiModelProperty(reference = "#/definitions/SynchronizationStatus")
public SynchronizationStatus getLdap1() {
return ldap1;
}
Based on this mailing list post from last year I believe it is not trivial and one may have to extend the appropriate Swagger resources. The only other option would be to manually reference the model as per Cássio Mazzochi Molin's answer (just be careful that renaming SynchronizationStatus doesn't break the API docs due to the forced use of a non-generated string)
I was able to achieve this with (using swagger-jaxrs-2.1.3)
System.setProperty(ModelResolver.SET_PROPERTY_OF_ENUMS_AS_REF, "true");
Reader reader = new Reader();
OpenAPI api = reader.read(...);
I am using a ISBNdB to get info about the books.The reponse type is application/octet-stream. A sample json response I get looks as follows
{
"index_searched" : "isbn",
"data" : [
{
"publisher_id" : "john_wiley_sons_inc",
"publisher_name" : "John Wiley & Sons, Inc",
"title_latin" : "Java programming interviews exposed",
"language" : "eng",
"summary" : "",
"physical_description_text" : "1 online resource (xvi, 368 pages) :",
"author_data" : [
{
"name" : "Markham, Noel",
"id" : "markham_noel"
},
{
"id" : "greg_milette",
"name" : "Greg Milette"
}
],
"title_long" : "Java programming interviews exposed",
"urls_text" : "",
"publisher_text" : "New York; John Wiley & Sons, Inc",
"book_id" : "java_programming_interviews_exposed",
"awards_text" : "; ",
"subject_ids" : [],
"isbn13" : "9781118722862",
"lcc_number" : "",
"title" : "Java programming interviews exposed",
"isbn10" : "1118722868",
"dewey_decimal" : "005.13/3",
"edition_info" : "; ",
"notes" : "\"Wrox programmer to programmer\"--Cover.; Acceso restringido a usuarios UCM = For UCM patrons only.",
"marc_enc_level" : "",
"dewey_normal" : "5.133"
}
]
}
I am using Jackson to convert this reponse. My Pojo looks as follows
#JsonIgnoreProperties(ignoreUnknown = true)
public class value {
private String index_searched;
// Another pojo in different file with ignore properties
private data[] dat;
public value(){
}
public data[] getDat() {
return dat;
}
public void setDat(data[] dat) {
this.dat = dat;
}
public String getIndex_searched() {
return index_searched;
}
public void setIndex_searched(String index_searched) {
this.index_searched = index_searched;
}
}
When I tried following
value book = restTemplate.getForObject(FINAL_URL, value.class);
I get this exception
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.rocketrenga.mylibrary.domain.value] and content type [application/octet-stream]
But I am able to map the response to String
String book = restTemplate.getForObject(FINAL_URL, String.class);
ObjectMapper mapper = new ObjectMapper();
value val = mapper.readValue(book, value.class);
System.out.println(val.getIndex_searched());
How to go about mapping the response directly POJO instead of String and converting back to POJO
You need to conifigure restTemplate with message converters. In your configuration do the following:
#Bean
public RestOperations restTemplate() {
RestTemplate restTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(
Arrays.asList(new MediaType[]{MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM}));
restTemplate.setMessageConverters(Arrays.asList(converter, new FormHttpMessageConverter()));
return restTemplate;
}
I guess the better solution is to just add another converter, not to modify current ones:
#Bean
public RestTemplate restTemplate() {
final RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(jacksonSupportsMoreTypes());
return restTemplate;
}
private HttpMessageConverter jacksonSupportsMoreTypes() {//eg. Gitlab returns JSON as plain text
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(MediaType.parseMediaType("text/plain;charset=utf-8"), MediaType.APPLICATION_OCTET_STREAM));
return converter;
}
I've got one Pojo object which is serialized by Jersey using jackson:
public class BookOfFriendsAnswer {
private Map<String, List<BookSummary>> books;
public BookOfFriendsAnswer() {
}
public BookOfFriendsAnswer(Map<String, List<BookSummary>> books) {
this.books = books;
}
public Map<String, List<BookSummary>> getBooks() {
return books;
}
public void setBooks(Map<String, List<BookSummary>> books) {
this.books = books;
}
}
The serialization produces a JSon like this one:
{
"books": {
"entry": [
{
"key": "54567bbce4b0e0ef9379993e",
"value": "BookSummary{id='54567bbde4b0e0ef9379993f', title='title 1', authors=[Steve,James] } BookSummary{id='54567bd9e4b0e0ef93799940', title='Title 2', authors=[Simon, Austin]}"
}
]
}
}
However, when I'm trying to deserialize the message from my client like this:
mapper.readValue(json, clazz)
I get the following error:
Unrecognized field "key" (class com.example.server.api.BookSummary), not marked as ignorable
I don't know if the problem comes from the JSOn produced by the server or the deserialization on client's side.
Do you know what is the problem and how to correct it?
Thanks a lot
So after a little testing with:
Jersey 1.18.1 (with jersey-json-1.18.1 for JSON support)
Jersey 2.13 (with jersey-media-json-jackson-2.13 for JSON support)
Jersey 2.13 (with jersey-media-moxy-2.13 for JSON support)
The last test (jersey-media-moxy-2.13) was the only one to produce this exact output
{
"books": {
"entry": [
{
"key": "54567bbce4b0e0ef9379993e",
"value": "BookSummary{id='54567bbde4b0e0ef9379993f', title='title 1', authors=[Steve,James] } BookSummary{id='54567bd9e4b0e0ef93799940', title='Title 2', authors=[Simon, Austin]}"
}
]
}
}
That being said, I'll make the assumption you're using a Jersey 2.x version. I'm not sure if there is any configuration in MOXy to better support this use case, but to make things easy, just add the following dependency, and get rid of the MOXy
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey-version}</version>
</dependency>
With this, you will get the correct JSON output
{ // BookOfFriendsAnswer object
"books": { // Map<String, List<BookSummary>> books
"randomKey": [ // String (key) , List<BookSummary> (value)
{ // BookSummary object
"id": "54567bbde4b0e0ef9379993f",
"title": "Title 1",
"authors": ["Steve", "James"]
}
]
}
}
Simple Test
Resource method
#GET
#Produces(MediaType.APPLICATION_JSON)
public Response getResponse() {
BookOfFriendsAnswer books = new BookOfFriendsAnswer();
String id = "randomKey"; <===== Not sure if you want the key to be
the BookSummary id
BookSummary summary = new BookSummary();
summary.setId(id);
summary.setTitle("Title 1");
summary.getAuthors().add("Steve");
summary.getAuthors().add("James");
List<BookSummary> summaries = new ArrayList<>();
summaries.add(summary);
books.getBooks().put("randomKey", summaries);
return Response.ok(books).build();
}
Test With ObjectMapper
#Test
public void testGetIt() throws Exception {
String responseMsg = target.path("book").request().get(String.class);
ObjectMapper mapper = new ObjectMapper();
BookOfFriendsAnswer books = mapper.readValue(
responseMsg, BookOfFriendsAnswer.class);
System.out.println(books);
}
Test Without ObjectMapper - Using the automatically configured Jackson provider
#Test
public void testGetIt() throws Exception {
BookOfFriendsAnswer responseMsg
= target.path("book").request().get(BookOfFriendsAnswer.class);
System.out.println(responseMsg);
}
I think that you should create specific Map type and provide it into deserialization process:
TypeFactory typeFactory = mapper.getTypeFactory();
MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, ArrayList.class);
HashMap<String, List<BookSummary>> map = mapper.readValue(json, mapType);