Skip to content

Commit 81f2de0

Browse files
feat: opt-in unknown key validation for ConfigBeanFactory (#851)
1 parent be40b4b commit 81f2de0

4 files changed

Lines changed: 97 additions & 13 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,9 @@ config matches the bean's implied schema. Bean fields can be
484484
primitive types, typed lists such as `List<Integer>`,
485485
`java.time.Duration`, `ConfigMemorySize`, or even a raw `Config`,
486486
`ConfigObject`, or `ConfigValue` (if you'd like to deal with a
487-
particular value manually).
487+
particular value manually). By default, config keys that do not map
488+
to a bean property are ignored. To reject unknown keys, use the
489+
`ConfigBeanFactory.create(config, MyBean.class, false)` overload.
488490

489491
## Using HOCON, the JSON Superset
490492

config/src/main/java/com/typesafe/config/ConfigBeanFactory.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,52 @@ public class ConfigBeanFactory {
3939
* @throws ConfigException.BadBean
4040
* If something is wrong with the JavaBean
4141
* @throws ConfigException.ValidationFailed
42-
* If the config doesn't conform to the bean's implied schema
42+
* If the config doesn't conform to the bean's implied schema. Unknown
43+
* config keys are ignored.
4344
* @throws ConfigException
4445
* Can throw the same exceptions as the getters on <code>Config</code>
4546
*/
4647
public static <T> T create(Config config, Class<T> clazz) {
47-
return ConfigBeanImpl.createInternal(config, clazz);
48+
return ConfigBeanImpl.createInternal(config, clazz, true);
49+
}
50+
51+
/**
52+
* Creates an instance of a class, initializing its fields from a {@link Config}.
53+
*
54+
* Example usage:
55+
*
56+
* <pre>
57+
* Config configSource = ConfigFactory.load().getConfig("foo");
58+
* FooConfig config = ConfigBeanFactory.create(configSource, FooConfig.class, false);
59+
* </pre>
60+
*
61+
* The Java class should follow JavaBean conventions. Field types
62+
* can be any of the types you can normally get from a {@link
63+
* Config}, including <code>java.time.Duration</code> or {@link
64+
* ConfigMemorySize}. Fields may also be another JavaBean-style
65+
* class.
66+
*
67+
* Fields are mapped to config by converting the config key to
68+
* camel case. So the key <code>foo-bar</code> becomes JavaBean
69+
* setter <code>setFooBar</code>.
70+
*
71+
* @since 1.4.9
72+
*
73+
* @param config source of config information
74+
* @param clazz class to be instantiated
75+
* @param allowUnknownConfigKeys if false, throw a validation error when
76+
* the config has keys that do not map to bean properties
77+
* @param <T> the type of the class to be instantiated
78+
* @return an instance of the class populated with data from the config
79+
* @throws ConfigException.BadBean
80+
* If something is wrong with the JavaBean
81+
* @throws ConfigException.ValidationFailed
82+
* If the config doesn't conform to the bean's implied schema, including
83+
* unknown config keys when <code>allowUnknownConfigKeys</code> is false
84+
* @throws ConfigException
85+
* Can throw the same exceptions as the getters on <code>Config</code>
86+
*/
87+
public static <T> T create(Config config, Class<T> clazz, boolean allowUnknownConfigKeys) {
88+
return ConfigBeanImpl.createInternal(config, clazz, allowUnknownConfigKeys);
4889
}
4990
}

config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class ConfigBeanImpl {
4040
* @param clazz class of the bean
4141
* @return the bean instance
4242
*/
43-
public static <T> T createInternal(Config config, Class<T> clazz) {
43+
public static <T> T createInternal(Config config, Class<T> clazz, boolean allowUnknownConfigKeys) {
4444
if (((SimpleConfig)config).root().resolveStatus() != ResolveStatus.RESOLVED)
4545
throw new ConfigException.NotResolved(
4646
"need to Config#resolve() a config before using it to initialize a bean, see the API docs for Config#resolve()");
@@ -76,10 +76,26 @@ public static <T> T createInternal(Config config, Class<T> clazz) {
7676
}
7777
beanProps.add(beanProp);
7878
}
79+
Set<String> beanPropNames = new HashSet<String>();
80+
for (PropertyDescriptor beanProp : beanProps) {
81+
beanPropNames.add(beanProp.getName());
82+
}
7983

8084
// Try to throw all validation issues at once (this does not comprehensively
8185
// find every issue, but it should find common ones).
8286
List<ConfigException.ValidationProblem> problems = new ArrayList<ConfigException.ValidationProblem>();
87+
if (!allowUnknownConfigKeys) {
88+
for (Map.Entry<String, String> nameEntry : originalNames.entrySet()) {
89+
String camelName = nameEntry.getKey();
90+
if (!beanPropNames.contains(camelName)) {
91+
AbstractConfigValue configValue = configProps.get(camelName);
92+
problems.add(new ConfigException.ValidationProblem(
93+
Path.newKey(nameEntry.getValue()).render(),
94+
configValue.origin(),
95+
"Unknown config setting"));
96+
}
97+
}
98+
}
8399
for (PropertyDescriptor beanProp : beanProps) {
84100
Method setter = beanProp.getWriteMethod();
85101
Class<?> parameterClass = setter.getParameterTypes()[0];
@@ -121,7 +137,8 @@ public static <T> T createInternal(Config config, Class<T> clazz) {
121137
// Otherwise, raise a {@link Missing} exception right here
122138
throw new ConfigException.Missing(beanProp.getName());
123139
}
124-
Object unwrapped = getValue(clazz, parameterType, parameterClass, config, configPropName);
140+
Object unwrapped = getValue(clazz, parameterType, parameterClass, config, configPropName,
141+
allowUnknownConfigKeys);
125142
setter.invoke(bean, unwrapped);
126143
}
127144
return bean;
@@ -145,7 +162,7 @@ public static <T> T createInternal(Config config, Class<T> clazz) {
145162
// types plus you can always use Object, ConfigValue, Config,
146163
// ConfigObject, etc. as an escape hatch.
147164
private static Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config,
148-
String configPropName) {
165+
String configPropName, boolean allowUnknownConfigKeys) {
149166
if (parameterClass == Boolean.class || parameterClass == boolean.class) {
150167
return config.getBoolean(configPropName);
151168
} else if (parameterClass == Integer.class || parameterClass == int.class) {
@@ -163,9 +180,11 @@ private static Object getValue(Class<?> beanClass, Type parameterType, Class<?>
163180
} else if (parameterClass == Object.class) {
164181
return config.getAnyRef(configPropName);
165182
} else if (parameterClass == List.class) {
166-
return getListValue(beanClass, parameterType, parameterClass, config, configPropName);
183+
return getListValue(beanClass, parameterType, parameterClass, config, configPropName,
184+
allowUnknownConfigKeys);
167185
} else if (parameterClass == Set.class) {
168-
return getSetValue(beanClass, parameterType, parameterClass, config, configPropName);
186+
return getSetValue(beanClass, parameterType, parameterClass, config, configPropName,
187+
allowUnknownConfigKeys);
169188
} else if (parameterClass == Map.class) {
170189
// we could do better here, but right now we don't.
171190
Type[] typeArgs = ((ParameterizedType)parameterType).getActualTypeArguments();
@@ -186,17 +205,20 @@ private static Object getValue(Class<?> beanClass, Type parameterType, Class<?>
186205
Enum enumValue = config.getEnum((Class<Enum>) parameterClass, configPropName);
187206
return enumValue;
188207
} else if (hasAtLeastOneBeanProperty(parameterClass)) {
189-
return createInternal(config.getConfig(configPropName), parameterClass);
208+
return createInternal(config.getConfig(configPropName), parameterClass, allowUnknownConfigKeys);
190209
} else {
191210
throw new ConfigException.BadBean("Bean property " + configPropName + " of class " + beanClass.getName() + " has unsupported type " + parameterType);
192211
}
193212
}
194213

195-
private static Object getSetValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPropName) {
196-
return new HashSet((List) getListValue(beanClass, parameterType, parameterClass, config, configPropName));
214+
private static Object getSetValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config,
215+
String configPropName, boolean allowUnknownConfigKeys) {
216+
return new HashSet((List) getListValue(beanClass, parameterType, parameterClass, config, configPropName,
217+
allowUnknownConfigKeys));
197218
}
198219

199-
private static Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPropName) {
220+
private static Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config,
221+
String configPropName, boolean allowUnknownConfigKeys) {
200222
Type elementType = ((ParameterizedType)parameterType).getActualTypeArguments()[0];
201223

202224
if (elementType == Boolean.class) {
@@ -229,7 +251,7 @@ private static Object getListValue(Class<?> beanClass, Type parameterType, Class
229251
List<Object> beanList = new ArrayList<Object>();
230252
List<? extends Config> configList = config.getConfigList(configPropName);
231253
for (Config listMember : configList) {
232-
beanList.add(createInternal(listMember, (Class<?>) elementType));
254+
beanList.add(createInternal(listMember, (Class<?>) elementType, allowUnknownConfigKeys));
233255
}
234256
return beanList;
235257
} else {

config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,25 @@ class ConfigBeanFactoryTest extends TestUtils {
7272
assertEquals("yes", beanConfig.getYes)
7373
}
7474

75+
@Test
76+
def testCreateAllowsUnknownConfigKeysByDefault() {
77+
val beanConfig = ConfigBeanFactory.create(parseConfig("{abcd=abcd, yes=yes, nope=nope}"),
78+
classOf[StringsConfig])
79+
assertNotNull(beanConfig)
80+
assertEquals("abcd", beanConfig.getAbcd)
81+
assertEquals("yes", beanConfig.getYes)
82+
}
83+
84+
@Test
85+
def testCreateFailsOnUnknownConfigKeysWhenNotAllowed() {
86+
val e = intercept[ConfigException.ValidationFailed] {
87+
ConfigBeanFactory.create(parseConfig("{abcd=abcd, yes=yes, nope=nope}"), classOf[StringsConfig],
88+
false)
89+
}
90+
assertTrue("unknown setting error", e.getMessage.contains("Unknown config setting"))
91+
assertTrue("error about the right property", e.getMessage.contains("nope"))
92+
}
93+
7594
@Test
7695
def testCreateEnum() {
7796
val beanConfig: EnumsConfig = ConfigBeanFactory.create(loadConfig().getConfig("enums"), classOf[EnumsConfig])

0 commit comments

Comments
 (0)