I have recently begun working with Spring Boot and am developing a mock application.
The frontend sends a GET request to the backend. I need to convert the request header into a custom class (CompoundSpecFactory). According to the official Spring Boot documentation (https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.spring-mvc.customize-jackson-objectmapper), one of the ways to do so is to customize the default converter provided by Jackson2ObjectMapperBuilder. This customization can take various forms (all listed in the link above). I've tried two so far and neither seems to work:
- creating a Jackson2ObjectMapperBuilderCustomizer bean;
- creating a com.fasterxml.jackson.databind.Module bean.
Both solutions require defining a custom deserializer that extends JsonDeserializer (https://fasterxml.github.io/jackson-databind/javadoc/2.13/com/fasterxml/jackson/databind/JsonDeserializer.html) and then adding this deserializer either to the BuilderCustomizer (option 1) or to the Module (option 2). As far as I understand, Spring Boot will then take care of actually customizing the default converter created by the Jackson2ObjectMapperBuilder.
The thing is, the input JSON is not converted to my custom class. Indeed, when I try and print the object that should result from the conversion, I get a null value.
While pursuing the second option (creating a module bean), I've tried adding a logging message to the deserialize() method of my custom deserializer, to check whether it was in fact called, and the message does not print to console. I'm assuming this means that the module is not registered with the Jackson2ObjectMapperBuilder, but I might be missing something. Can anybody tell what I'm getting wrong?
My module:
@Component
public class CompoundSpecModule<B extends Building> extends SimpleDeserializers {
/**
*
*/
private static final long serialVersionUID = 1L;
public CompoundSpecModule() {
this.addDeserializer(CompoundSpecFactory.class, new CriteriaDeserializer<B>());
}
}
My custom deserializer:
public class CriteriaDeserializer<B extends Building> extends StdDeserializer<CompoundSpecFactory<B>> {
/**
*
*/
private static final long serialVersionUID = 1L;
protected CriteriaDeserializer() {
super(CompoundSpecFactory.class);
}
@Override
public Class<?> handledType() {
return CompoundSpecFactory.class;
}
@Override
public CompoundSpecFactory<B> deserialize(JsonParser p, DeserializationContext ctx)
throws IOException, JacksonException {
/**/ System.out.println("deserialize has been called");
JsonNode root = p.getCodec().readTree(p);
List<String> criterionTypes = this.getCriterionTypes(root.get("criterionType"));
List<String> dimensions = this.getDimensions(root.get("dimension"));
List<List<String>> parameters = this.getParameters(root.get("parameters"));
ArrayList<BuildingSpec<B>> specs = this.getSpecifications(criterionTypes, dimensions, parameters);
return new CompoundSpecFactory<B>(specs);
}
... //
}
My SpringBootApplication class:
@SpringBootApplication
public class BuildingsApplication {
public static void main(String[] args) {
SpringApplication.run(BuildingsApplication.class, args);
}
}
My controller:
@RestController
@RequestMapping("/building")
public class BuildingController {
private BuildingService buildingService;
public BuildingController(BuildingService bs) {
buildingService = bs;
}
@GetMapping("/getForRentBuildings")
public List<ForRentBuilding> getForRentBuildings(CompoundSpecFactory<ForRentBuilding> compSpecFactory){
return buildingService.getForRentBuildings(compSpecFactory);
}
}
Example of request URL:
http://localhost:4200/buildings/building/getForRentBuildings?criterionType=equals&criterionType=equals&dimension=street&dimension=city¶meters=Brighton%20Street¶meters=London
CompoundSpecFactory (the Java class I want the URL to convert to):
public class CompoundSpecFactory<B extends Building>{
private Specification<B> compoundSpec;
public CompoundSpecFactory() {
}
@SuppressWarnings("unchecked")
public CompoundSpecFactory(BuildingSpec<B> ...buildingSpecs) {
compoundSpec = buildCompoundSpecification(buildingSpecs);
}
public CompoundSpecFactory(List<BuildingSpec<B>>buildingSpecs) {
compoundSpec = buildCompoundSpecification(buildingSpecs);
}
@SuppressWarnings("unchecked")
public Specification<B> buildCompoundSpecification(BuildingSpec<B> ...buildingSpecs) {
Specification<B> res = Specification.where(null);
for(BuildingSpec<B> bs: buildingSpecs) {
res = res.and(bs);
}
return res;
}
public Specification<B> buildCompoundSpecification(List<BuildingSpec<B>>buildingSpecs) {
Specification<B> res = Specification.where(null);
for(BuildingSpec<B> bs: buildingSpecs) {
res = res.and(bs);
}
return res;
}
public Specification<B> getCompoundSpec() {
return compoundSpec;
}
}
The latter might not be entirely clear: in a nutshell, the frontend conceptually sends search criteria (broken down in 'criterionType', 'dimension' and 'parameters'). In the backend, each search criterion is supposed to be converted into a Specification (see https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/domain/Specification.html). Specification's are then combined into a single Specification (which I call compound specification) through the above method buildCompoundSpecification(). There are reasons for all of the above choices (having a single Specification instead of separate ones, having a factory instead of a CompoundSpecification class directly), but I won't go into them unless they're deemed relevant by users so as not to clutter the post.
For the specific URL I report above, I want a compound specification representing the criterion "The building city ["dimension" in the search criterion breakdown] is equal ["criterionType"] to 'London' ["parameter"] AND the building street is equal to 'Brighton Street' ".