AldousP
AldousP

Reputation: 41

Conditional JsonProperty using Jackson with Spring Boot

A Spring Boot application is tasked with updating a remote integration API every so many minutes. This application can be deployed to a test or prod environment, the application is informed of the end point it should be looking at through an "application.properties" flag. A POJO is being serialized with Jackson and pushed to the endpoint, with the JsonProperty annotations containing the field IDs for the API that it is being pushed to.

ie

@JsonProperty("field_001)
private String name;

@JsonProperty("field_002)
private String address;

The field labels for these values differ on the test endpoint. So the test endpoint might expect the properties to map as

@JsonProperty("field_005)
private String name;

@JsonProperty("field_006)
private String address;

I would like to be able to utilize the Spring Boot native support for profile based properties files. To read in the JsonProperty annotation values at run time from an external properties file.

So for example,

There might be three files application.properties, application-test.properties and application-prod.properties. Spring Boot could read in the test or prod properties in addition to the vanilla properties file based on the "spring.profiles.active" setting.

...-test.properties would contain the constant values for the test server fields. And ...-prod.properties would contain the constant values for the prod server fields.

Nesting annotations such as Spring's @Value tag, like this:

@JsonProperty(@Value("${property.file.reference.here})) 

doesn't seem to work.

Upvotes: 4

Views: 8723

Answers (3)

Alexey Osetsky
Alexey Osetsky

Reputation: 178

This is Matiz 's augmented answer.

Solution using extended JacksonAnnotationIntrospector which allows to use ${environment.properties} within @JsonProperty annotation

public class DynamicJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector {
    private final Environment environment;

    public DynamicJacksonAnnotationIntrospector(Environment environment) {
        this.environment = environment;
    }

    @Override
    public PropertyName findNameForSerialization(Annotated a) {
        return injectEnvironmentInJsonProperty(super.findNameForSerialization(a));
    }

    @Override
    public PropertyName findNameForDeserialization(Annotated a){
        return injectEnvironmentInJsonProperty(super.findNameForDeserialization(a));
    }

    @Override
    public PropertyName findRootName(AnnotatedClass ac) {
        return injectEnvironmentInJsonProperty(super.findNameForDeserialization(ac));
    }

    private PropertyName injectEnvironmentInJsonProperty(PropertyName name){
        if (name == null) {
            return null;
        }
        String simpleName = name.getSimpleName();
        log.info(environment.resolvePlaceholders(simpleName));
        return PropertyName.construct(environment.resolvePlaceholders(simpleName), name.getNamespace());
    }
}

Create webconfig class for entity recognition in controller.

@EnableWebMvc
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final ApplicationContext context;

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //JSON
        AnnotationIntrospector pairedIntrospectors = AnnotationIntrospector.pair(introspector(context.getEnvironment()),
                new JacksonAnnotationIntrospector());
        converters.add(new MappingJackson2HttpMessageConverter(
                Jackson2ObjectMapperBuilder.json()
                        .annotationIntrospector(pairedIntrospectors)
                        .build()));
    }

    @Bean
    public DynamicJacksonAnnotationIntrospector introspector(Environment environment) {
        return new DynamicJacksonAnnotationIntrospector(environment);
    }

    @Bean
    @Primary
    public ObjectMapper getObjectMapper(DynamicJacksonAnnotationIntrospector introspector) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        SerializationConfig serializationConfig = mapper.getSerializationConfig()
                .withInsertedAnnotationIntrospector(introspector);
        mapper.setConfig(serializationConfig);
        DeserializationConfig deserializationConfig = mapper.getDeserializationConfig()
                .withInsertedAnnotationIntrospector(introspector);
        mapper.setConfig(deserializationConfig);
        return mapper;
    }
}

And disable class in autoconfiguration for spring boot

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

Examples

@Getter
@Setter
public class DynamicTestClass {
    @JsonProperty("${dynamic.property.name}")
    private String dynamicPropertyName;
}
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = JiraSmCoreApplication.class)
@TestPropertySource("classpath:application-test.yml")
public class DynamicJacksonAnnotationIntrospectorTest {
    @Autowired
    MappingJackson2HttpMessageConverter mapper;

    @Test
    public void shouldFindNameForSerializationFromProperties() throws JsonProcessingException {
        DynamicTestClass bean = new DynamicTestClass();
        bean.setDynamicPropertyName("qwerty");
        log.info(mapper.getObjectMapper().writeValueAsString(bean));
    }
}

application-test.yml

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

dynamic:
  property:
    name: overriddenName

Upvotes: 0

Matiz
Matiz

Reputation: 51

I apologize for reviving an old question however I still was not able to find satisfying answer.

Here's my solution using extended JacksonAnnotationIntrospector which allows to use ${environment.properties} within @JsonProperty annotation

First extend the introspector

public class DynamicJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector {
    private final Environment environment;

    public DynamicJacksonAnnotationIntrospector(Environment environment) {
        this.environment = environment;
    }

    @Override
    public PropertyName findNameForSerialization(Annotated a) {
        PropertyName name = super.findNameForSerialization(a);
        if (name == null) {
            return null;
        }
        String simpleName = name.getSimpleName();
        return PropertyName.construct(environment.resolvePlaceholders(simpleName), name.getNamespace());
    }
    //For deserialization I think the same mechanism could be used,
    //just override `findNameForDeserialization`, although I haven't tested it
}

Then use it with ObjectMapper configuration

@Configuration
public class ObjectMapperConfiguration {
    @Bean
    public ObjectMapper getObjectMapper(DynamicJacksonAnnotationIntrospector introspector) {
        ObjectMapper mapper = new ObjectMapper();
        SerializationConfig config = mapper.getSerializationConfig().withInsertedAnnotationIntrospector(introspector);
        mapper.setConfig(config);
        return mapper;
    }

    @Bean
    public DynamicJacksonAnnotationIntrospector introspector(Environment environment) {
        return new DynamicJacksonAnnotationIntrospector(environment);
    }
}

Examples:

public class DynamicTestClass {
    @JsonProperty("${dynamic.property.name}")
    private String dynamicPropertyName;
    //getters/setters
}
@ContextConfiguration(classes = [
        ObjectMapperConfiguration
])
@TestPropertySource("classpath:test.properties")
class DynamicJacksonAnnotationIntrospectorTest extends Specification {
    @Autowired
    ObjectMapper mapper

    def "should find name for serialization from properties"() {
        def bean = new DynamicTestClass()
        bean.dynamicPropertyName = "qwerty"

        when:
        def result = mapper.writeValueAsString(bean)

        then:
        result == "{\"overriddenName\":\"qwerty\"}"
    }
}

test.properties

dynamic.property.name=overriddenName

The solution is reverse compatible so you can still use constant values in @JsonProperty

Upvotes: 5

nicholas.hauschild
nicholas.hauschild

Reputation: 42849

I doubt you will be able to do this using Spring Expression Language (SpEL) inside of a Jackson annotation, as you are trying (with or without the @Value annotation).

I would do this by creating a JsonSerializer<YourPojo> and/or JsonDeserializer<YourPojo> that takes in your SpEL expressions and creates (or reads) using the provided field names.

//make me a spring managed bean!
public class PojoSerializer extends JsonSerializer<YourPojo> {
    @Value("${property.file.reference.name")
    private String nameField;

    @Value("${property.file.reference.address")
    private String addrField;

    @Override
    public void serialize(YourPojo pojo, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
        jgen.writeStartObject();
        jgen.writeStringField(nameField, pojo.getName());
        jgen.writeStringField(addrField, pojo.getAddress());
        jgen.writeEndObject();
    }
}

Being that this is a Spring managed bean, you would need to plug this into your Spring managed ObjectMapper.

ObjectMapper mapper = //my ObjectMapper from spring
PojoSerializer pojoSerializer = //my PojoSerializer from spring

SimpleModule module = new SimpleModule("MyModule", new Version(1, 0, 0, null));
module.addSerializer(YourPojo.class, pojoSerializer);
mapper.registerModule(module);

Some of this might not be necessary with SpringBoot's AutoConfiguration. I am generally unaware to what SpringBoot will pick up for its Jackson AutoConfiguration, but JsonSerializer and JsonDeserializer might be autoregistered if they are in the ApplicationContext.

Upvotes: 1

Related Questions