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.

Job Done