Situation

I have a Single Page App and a Spring Boot backend. From the Backend I want to put some properties through to the Frontend. As thr Frontend is JS, I use Jackson to produce JSON from the Java Beans.

Most of my properties are only used by the frontend so that I don't have Java-Beans for all of the Properties, and it is not suitable to create them all.

So I hava a .yml File like this:

gui:
  service:
    contact:
      salutations:
        - Mr.
        - Mrs.
        - ...

What I've done so far

Therefore I have a Java Class

@Component
@ConfigurationProperties(prefix = "gui")
public class CosGuiProperties implements CosProperties {
    private Map<String, Object> service;
}

I also have configrued my Application to read the yml-Files correctly into my application and that works fine, except of one exception:

The Problem

In Java, Springs makes from the List (Mr., Mrs, ...) a LinkedHashMap (1 => Mr., 2 => Mrs., 3 => ...) but I need List (and that is what I expect, because from the yml point of view it is a List). I debuged the Spring-Code and found the code-line where the Map is created.

Where the Map comes from

The Map was created in org.springframework.beans.factory.config.YamlProcessor#300 with the comment Need a compound key

The Question:

Is it possible to keep the ArrayList (wich is the datatype from yml) and don't convert it to a Map with a compound key?

Comment From: snicoll

Didn't I answer to that already in the issue you've created in the Spring Framework tracker?

Having said that Map is too generic I am afraid so if you open an issue there, I am not sure we'll act on it. Your map should be mapped on gui.service.contact instead of being mapped on gui.service.

Comment From: emasch

You said, that I should raise a Ticket on the Github Issuetracker:

this is the issue tracker of the Spring Framework, please use the Spring Boot issue tracker instead.

The Problem is, that I can't map on a more specific property since the Structure should be generic and not bound to a Java-Class as we only put it through to the frontend.

In the Class org.springframework.beans.factory.config.YamlProcessor#300 there's an explicit mapping from ArrayList to Map with a compound key. The question is if it is really necessary and why. Can't it be untouchted and be an ArrayList?

Comment From: wilkinsona

The problem is that by using Map<String, Object> you're not providing any hints about the format of the data and how you'd like it to be bound. Rather than using Object, you should use a series of rich types for your configuration. In Boot, we often structure these as nested inner classes. In your case, I'd expect to see something like this:

package com.example;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("gui")
public class GuiProperties {

    private Service service = new Service();

    public Service getService() {
        return service;
    }

    public void setService(Service service) {
        this.service = service;
    }

    static class Service {

        private Contact contact = new Contact();

        public Contact getContact() {
            return contact;
        }

        public void setContact(Contact contact) {
            this.contact = contact;
        }

        static class Contact {

            private List<String> salutations = new ArrayList<>();

            public List<String> getSalutations() {
                return salutations;
            }

            public void setSalutations(List<String> salutations) {
                this.salutations = salutations;
            }

        }

    }

}

The Problem is, that I can't map on a more specific property since the Structure should be generic and not bound to a Java-Class as we only put it through to the frontend.

It sounds like you're trying to mix a type that's used for configuration properties with a type that's part of your service's API. I wouldn't recommend doing that as it restrict what you can change without breaking something. Configuration data is structured and, as described above, there are benefits to provide a set of rich types for it so I would recommend that you do so.

If you also need to send that configuration data to a front end, then you should map it to another type that's specifically part of the service's public API or plug in some custom serialisation so that the format of your configuration data isn't directly coupled to the format of the responses sent to your front end.

Comment From: vasilievip

package com.acme.properties

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.validation.annotation.Validated

@Validated
@ConfigurationProperties(prefix = "acme")
class ConfigurationProperties {

    Map<String, Object> props

    Map getProps() {
        return convertSpringPropertiesToMap(props) as Map
    }

    private Object convertSpringPropertiesToMap(Map o) {
        Map result = new HashMap()
        o.each { key, value ->
            if (value instanceof Map) {
                if (value.iterator()[0].key as String =~ /^\d$/) {
                    result[key] = convertSpringMapToList(value)
                    return
                } else {
                    result[key] = convertSpringPropertiesToMap(value)
                }
            } else {
                result[key] = value
            }
        }
        return result
    }

    private List<Object> convertSpringMapToList(Map<String, ?> springMap) {
        List result = []
        springMap.each { key, value ->
            if (value instanceof Map) {
                result.add(convertSpringPropertiesToMap(value as Map))
            } else {
                result.add(value)
            }
        }
        return result
    }

}

workaround from @anjkl

Comment From: puneetbehl

We are facing a similar problem with the Grails framework. We have the following class:

import grails.util.TypeConvertingMap
import groovy.transform.CompileStatic
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.web.cors.CorsConfiguration

@CompileStatic
@ConfigurationProperties(prefix = 'grails.cors')
class GrailsCorsConfiguration {

    Boolean enabled = false

    @Delegate
    GrailsDefaultCorsConfiguration grailsCorsMapping = new GrailsDefaultCorsConfiguration()

    Map<String, Map<String, List<String>>> mappings = [:]

    Map<String, CorsConfiguration> getCorsConfigurations() {
        grailsCorsMapping.applyPermitDefaultValues()
        Map<String, CorsConfiguration> corsConfigurationMap = [:]

        if (enabled) {
            if (mappings.size() > 0) {
                mappings.each { String key, Object value ->
                    GrailsDefaultCorsConfiguration corsConfiguration = new GrailsDefaultCorsConfiguration(grailsCorsMapping)
                    if (value instanceof Map) {
                        TypeConvertingMap config = new TypeConvertingMap((Map)value)
                        if (config.containsKey('allowedOrigins')) {
                            corsConfiguration.allowedOrigins = config.list('allowedOrigins')
                        }
                        if (config.containsKey('allowedMethods')) {
                            corsConfiguration.allowedMethods = config.list('allowedMethods')
                        }
                        if (config.containsKey('allowedHeaders')) {
                            corsConfiguration.allowedHeaders = config.list('allowedHeaders')
                        }
                        if (config.containsKey('exposedHeaders')) {
                            corsConfiguration.exposedHeaders = config.list('exposedHeaders')
                        }
                        if (config.containsKey('maxAge')) {
                            corsConfiguration.maxAge = config.long('maxAge')
                        }
                        if (config.containsKey('allowCredentials')) {
                            corsConfiguration.allowCredentials = config.boolean('allowCredentials')
                        }
                    }
                    corsConfigurationMap[key] = corsConfiguration
                }
            } else {
                corsConfigurationMap["/**"] = grailsCorsMapping
            }
        }

        corsConfigurationMap
    }
}

And value from YAML configuration file is:

grails:
    cors:
        enabled: true
        allowedHeaders:
            - Content-Type
        mappings:
            '[/word/googleOnly]':
                allowedOrigins:
                    - https://www.google.com
            '[/word/stackoverflowOnly]':
                allowedOrigins:
                    - https://stackoverflow.com
            '[/word/anywhere]':
                allowedOrigins:
                    - '*'

Upon debugging, the value bound to the field mappings is not as expected, please check the following screenshot:

Screenshot 2022-11-04 at 3 00 09 PM

Comment From: wilkinsona

@puneetbehl This issue's over six years old. Without knowing which version of Boot you're using beneath Grails, it's hard to offer much advice beyond what's already been stated above. It looks to me like you are not providing enough type information to allow the properties to be bound as you would like. If so, that will have to be fixed in Grails. If you think your situation's different, please open a new issue, providing a minimal, non-Grails application that reproduces the problem.

Comment From: puneetbehl

Thank you for the quick response. I believe this is replicable on Grails 5.2.4 which is using Spring Boot 2.7.0. Here is the sample application https://github.com/puneetbehl/corsexample

Comment From: wilkinsona

Unless the problem is somehow specific to Grails, a minimal sample should not depend on Grails. You also haven't addressed whether you're providing enough type information in the configuration properties class. If you believe the configuration properties class is providing sufficient type information, i.e. it's binding to something more detailed than Object, and you would like us to spend some time investigating, please spend some time providing a complete yet minimal sample that reproduces the problem and open a new issue referencing it.