Use a custom serializer to write policy config values as JSON array objects instead of a stringfied array

Closes #44573

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2025-12-15 18:51:08 -03:00
parent e8c6a7b98d
commit 032158901b
3 changed files with 121 additions and 2 deletions

View file

@ -0,0 +1,62 @@
package org.keycloak.representations.idm.authorization;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
public class PolicyConfigDeserializer extends StdDeserializer<Map<String,String>> {
public PolicyConfigDeserializer() {
this(null);
}
public PolicyConfigDeserializer(Class<Map<String, String>> t) {
super(t);
}
@Override
public Map<String, String> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
// ensure we are at the start of the JSON object for the map
if (parser.currentToken() != JsonToken.START_OBJECT) {
context.reportWrongTokenException(Map.class, JsonToken.START_OBJECT,
"Expected START_OBJECT for config map");
}
Map<String, String> map = new HashMap<>();
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
// loop through key-value pairs in the JSON object
while (parser.nextToken() != JsonToken.END_OBJECT) {
// get the key and value
String key = parser.currentName();
parser.nextToken();
// check the type of the value token
if (parser.currentToken() == JsonToken.START_ARRAY) {
// case 1: value is a JSON array
ArrayNode arrayNode = mapper.readTree(parser);
// convert the ArrayNode back into a single string (the original format)
String originalStringFormat = arrayNode.toString();
map.put(key, originalStringFormat);
} else if (parser.currentToken() == JsonToken.VALUE_STRING) {
// case 2: value is a JSON String (the regular case)
String value = parser.getText();
map.put(key, value);
} else {
// handle other unexpected types, if necessary
context.reportWrongTokenException(Map.class, parser.currentToken(),
"Expected String or Array for config value");
}
}
return map;
}
}

View file

@ -0,0 +1,52 @@
package org.keycloak.representations.idm.authorization;
import java.io.IOException;
import java.util.Map;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
public class PolicyConfigSerializer extends StdSerializer<Map<String, String>> {
private static final ObjectMapper MAPPER = new ObjectMapper();
public PolicyConfigSerializer() {
this(null);
}
public PolicyConfigSerializer(Class<Map<String, String>> t) {
super(t);
}
@Override
public void serialize(Map<String, String> map, JsonGenerator generator, SerializerProvider provider) throws IOException {
generator.writeStartObject();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
generator.writeFieldName(key);
// check if the value string looks like a JSON array
if (value != null && value.startsWith("[\"") && value.endsWith("\"]")) {
try {
// attempt to read the string as a JSON node (which should be an ArrayNode)
ArrayNode arrayNode = (ArrayNode) MAPPER.readTree(value);
// write the ArrayNode directly to the generator
generator.writeTree(arrayNode);
} catch (Exception e) {
// if parsing fails, write the value as a plain string
generator.writeString(value);
}
} else {
// not an array string, so write as a plain string
generator.writeString(value);
}
}
generator.writeEndObject();
}
}

View file

@ -19,12 +19,17 @@ package org.keycloak.representations.idm.authorization;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyRepresentation extends AbstractPolicyRepresentation {
private Map<String, String> config = new HashMap();
@JsonSerialize(using = PolicyConfigSerializer.class)
@JsonDeserialize(using = PolicyConfigDeserializer.class)
private Map<String, String> config = new HashMap<>();
public Map<String, String> getConfig() {
return this.config;
@ -33,4 +38,4 @@ public class PolicyRepresentation extends AbstractPolicyRepresentation {
public void setConfig(Map<String, String> config) {
this.config = config;
}
}
}