Dealing with configuration list in Micronaut
Recently I wanted to map configuration list within the application.yml
into an immutable configuration object of a Micronaut application. The YAML configuration is fairly simple. It defines a list of instances containing fields such as name
, version
, endpoint
and read-timeout
.
a:
b:
instances:
- name: T1
endpoint: "http://t1"
version: "1.5.3.24305,2021-08-09 18:01"
read-timeout: 20
- name: T2
endpoint: "http://t1"
version: "2.0.0.16555,2022-01-03 16:48"
read-timeout: 20
Now I created an immutable Java configuration and wanted Micronaut to bind the YAML onto my configuration.
@ConfigurationProperties("a.b")
public interface MyConfig {
List<Instance> getInstances();
interface Instance {
String getName();
URL getEndpoint();
String getVersion();
int getReadTimeout();
}
}
Easy, so far. As good developers we are used to write tests and this is what I did to verify whether everything is working as expected.
@MicronautTest
class MyConfigSpec extends Specification {
@Inject
MyConfig config
void "Make sure the config has instances"() {
expect:
config.instances.isEmpty() == false
}
}
Guess what. The test failed 🥺. getInstances()
always returns an empty list no matter what I do. After debugging a while I realized that Jackson ObjectMapper is trying to create an instance of Instance
but obviously can't since there is no implementation present. But migrating the interface Instance
into a concrete class did work either. At that point I was not sure what's really going on here so I posted my problem to Stack Overflow. Within less than 4 hours my problem was solved.
The solution is simple. We need to help Micronaut and introduce a dedicated TypeConverter
that allows Micronaut to convert a Map
into an Instance
. Here is an example of Szymon Stepniak.
@Singleton
class MapToInstanceConverter implements TypeConverter<Map, Instance> {
@Override
public Optional<Instance> convert(Map object, Class<Instance> targetType, ConversionContext context) {
return Optional.of(new Instance() {
@Override
public String getName() {
return object.getOrDefault("name", "").toString();
}
@Override
public URL getEndpoint() {
try {
return new URI(object.getOrDefault("endpoint", "").toString()).toURL();
} catch (MalformedURLException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
@Override
public String getVersion() {
return object.getOrDefault("version", "").toString();
}
@Override
public int getReadTimeout() {
return Integer.parseInt(object.getOrDefault("read-timeout", 0).toString());
}
});
}
}
This converter made my test go green. Configuration binding done! Hope this helps.