From 1ccfcecb9e92ea5afd0be437989d6f0512d67c5e Mon Sep 17 00:00:00 2001 From: Sebastian Spaink Date: Wed, 24 Jun 2026 09:08:18 -0500 Subject: [PATCH] Migrate to jackson 3 Signed-off-by: Sebastian Spaink --- cli/build.gradle.kts | 3 +- .../open_policy_agent/opa/cli/Eval.java | 16 +- .../opa-builtins-json/build.gradle.kts | 10 +- .../opa/ast/builtin/impls/JsonBuiltins.java | 422 ++++++++++++------ .../opa-builtins-token/build.gradle.kts | 3 +- .../opa/ast/builtin/impls/TokenBuiltins.java | 42 +- opa-evaluator/build.gradle.kts | 3 +- .../bundle/FileSystemBundleLoaderTest.java | 7 +- .../NdBuiltinCacheTrackingTest.java | 7 +- .../opa/ir/ComplianceTest.java | 59 ++- .../opa/ir/EvaluatorTest.java | 5 +- .../opa/metrics/MetricsIntegrationTest.java | 7 +- ...impleStatementProfilerIntegrationTest.java | 5 +- .../opa/rego/CrossBundleDataTest.java | 7 +- .../opa/rego/EngineEvaluateTest.java | 7 +- .../opa/rego/EngineHotReloadTest.java | 7 +- .../opa/rego/EngineTest.java | 7 +- .../opa/rego/JsonNodeBridge.java | 7 +- .../DurationProfilerIntegrationTest.java | 5 +- opa-jackson/build.gradle.kts | 6 +- .../opa/jackson/IrModule.java | 8 +- .../opa/jackson/JacksonBundleParser.java | 11 +- .../opa/jackson/JacksonCapabilities.java | 13 +- .../opa/jackson/JacksonPolicyReader.java | 21 +- .../opa/jackson/MetricsModule.java | 5 +- .../opa/jackson/OpaCoverageReport.java | 6 +- .../opa/jackson/OperandDeserializer.java | 26 +- .../opa/jackson/RegoValueModule.java | 100 ++--- .../opa/jackson/StmtDeserializer.java | 54 +-- .../opa/jackson/TypeDeserializer.java | 52 +-- ...e => tools.jackson.databind.JacksonModule} | 0 .../opa/jackson/MetricsModuleTest.java | 15 +- .../opa/jackson/OpaCoverageReportTest.java | 4 +- .../opa/jackson/RegoValueModuleTest.java | 7 +- opa-services/build.gradle.kts | 7 +- .../io/github/open_policy_agent/opa/Opa.java | 29 +- .../open_policy_agent/opa/config/Config.java | 2 +- .../opa/config/ServicesDeserializer.java | 25 +- .../opa/plugins/DecisionLogPlugin.java | 25 +- .../opa/plugins/DiscoveryPlugin.java | 9 +- .../opa/plugins/StatusPlugin.java | 13 +- .../opa/OpaHotReloadTest.java | 7 +- .../github/open_policy_agent/opa/OpaTest.java | 4 +- .../opa/OpaTwoPhaseInitializationTest.java | 2 +- .../opa/config/ConfigTest.java | 4 +- .../opa/config/ServicesDeserializerTest.java | 4 +- .../opa/plugins/DecisionLogPluginTest.java | 4 +- .../opa/plugins/DiscoveryPluginTest.java | 2 +- .../opa/plugins/StatusPluginTest.java | 4 +- 49 files changed, 602 insertions(+), 496 deletions(-) rename opa-jackson/src/main/resources/META-INF/services/{com.fasterxml.jackson.databind.Module => tools.jackson.databind.JacksonModule} (100%) diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 86e0798a..ba46f6f3 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -9,7 +9,8 @@ repositories { dependencies { implementation(project(":opa-evaluator")) implementation(project(":opa-services")) - implementation("com.fasterxml.jackson.core:jackson-databind:2.21.3") + implementation(platform("tools.jackson:jackson-bom:3.2.0")) + implementation("tools.jackson.core:jackson-databind") implementation("info.picocli:picocli:4.7.7") implementation("org.apache.commons:commons-compress:1.28.0") diff --git a/cli/src/main/java/io/github/open_policy_agent/opa/cli/Eval.java b/cli/src/main/java/io/github/open_policy_agent/opa/cli/Eval.java index 41cf2202..7317ff95 100644 --- a/cli/src/main/java/io/github/open_policy_agent/opa/cli/Eval.java +++ b/cli/src/main/java/io/github/open_policy_agent/opa/cli/Eval.java @@ -1,7 +1,5 @@ package io.github.open_policy_agent.opa.cli; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; import io.github.open_policy_agent.opa.ast.builtin.BuiltinRegistry; import io.github.open_policy_agent.opa.bundle.FileSystemBundleLoader; import io.github.open_policy_agent.opa.bundle.TarballBundleLoader; @@ -31,6 +29,10 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.json.JsonMapper; @Command(name = "eval", mixinStandardHelpOptions = true) public class Eval implements Callable { @@ -134,8 +136,8 @@ public Integer call() { if (showCapabilities) { try { - System.out.println(new ObjectMapper().writeValueAsString(BuiltinRegistry.generateCapabilities())); - } catch (IOException e) { + System.out.println(JsonMapper.shared().writeValueAsString(BuiltinRegistry.generateCapabilities())); + } catch (JacksonException e) { System.err.println(e.getMessage()); return 1; } @@ -181,7 +183,7 @@ public Integer call() { showMetrics = true; } - final ObjectMapper objectMapper = new ObjectMapper(); + final ObjectMapper objectMapper = JsonMapper.shared(); final List allMetrics = new ArrayList<>(count); final List allTracers = new ArrayList<>(count); @@ -217,7 +219,7 @@ public Integer call() { } else { inputDoc = objectMapper.readValue(this.input.toFile(), Object.class); } - } catch (IOException e) { + } catch (JacksonException e) { System.err.println("Error reading input: " + e.getMessage()); return 1; } @@ -332,7 +334,7 @@ public Integer call() { ? objectMapper.writerWithDefaultPrettyPrinter() : objectMapper.writer(); System.out.println(writer.writeValueAsString(lastResults)); - } catch (IOException e) { + } catch (JacksonException e) { System.err.println("Error serializing results: " + e.getMessage()); return 1; } diff --git a/opa-builtins/opa-builtins-json/build.gradle.kts b/opa-builtins/opa-builtins-json/build.gradle.kts index ffbf26bd..4e586b0c 100644 --- a/opa-builtins/opa-builtins-json/build.gradle.kts +++ b/opa-builtins/opa-builtins-json/build.gradle.kts @@ -9,12 +9,10 @@ repositories { dependencies { api(project(":opa-evaluator")) - implementation("com.github.java-json-tools:json-patch:1.13") - implementation("com.networknt:json-schema-validator:1.5.5") - implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.0")) - implementation("com.fasterxml.jackson.core:jackson-databind") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.networknt:json-schema-validator:3.0.5") + implementation(platform("tools.jackson:jackson-bom:3.2.0")) + implementation("tools.jackson.core:jackson-databind") + implementation("tools.jackson.dataformat:jackson-dataformat-yaml") // RegoValueModule (auto-registered via Jackson SPI) provides (de)serializers for the AST // types so they don't need to carry annotations. runtimeOnly(project(":opa-jackson")) diff --git a/opa-builtins/opa-builtins-json/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/JsonBuiltins.java b/opa-builtins/opa-builtins-json/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/JsonBuiltins.java index 47b1f2eb..f1eb7c6e 100644 --- a/opa-builtins/opa-builtins-json/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/JsonBuiltins.java +++ b/opa-builtins/opa-builtins-json/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/JsonBuiltins.java @@ -1,29 +1,11 @@ package io.github.open_policy_agent.opa.ast.builtin.impls; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.util.DefaultIndenter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; -import com.github.fge.jsonpatch.JsonPatch; -import com.github.fge.jsonpatch.JsonPatchException; -import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SpecVersion; -import com.networknt.schema.ValidationMessage; -import java.io.IOException; -import java.util.*; -import java.util.function.BiFunction; +import static io.github.open_policy_agent.opa.ast.builtin.impls.utils.ArgHelper.getArg; + +import com.networknt.schema.Error; +import com.networknt.schema.Schema; +import com.networknt.schema.SchemaRegistry; +import com.networknt.schema.SpecificationVersion; import io.github.open_policy_agent.opa.ast.builtin.BuiltinError; import io.github.open_policy_agent.opa.ast.builtin.BuiltinProvider; import io.github.open_policy_agent.opa.ast.builtin.OpaBuiltin; @@ -31,8 +13,24 @@ import io.github.open_policy_agent.opa.ast.types.*; import io.github.open_policy_agent.opa.rego.EvaluationContext; import io.github.open_policy_agent.opa.rego.TypeError; - -import static io.github.open_policy_agent.opa.ast.builtin.impls.utils.ArgHelper.getArg; +import java.util.*; +import java.util.function.BiFunction; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.util.DefaultIndenter; +import tools.jackson.core.util.DefaultPrettyPrinter; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.dataformat.yaml.YAMLMapper; +import tools.jackson.dataformat.yaml.YAMLWriteFeature; public class JsonBuiltins implements BuiltinProvider { @@ -40,59 +38,53 @@ public class JsonBuiltins implements BuiltinProvider { private static final ObjectMapper YAML_MAPPER; // Shared serializers for Rego numeric types - private static final JsonSerializer REGO_DECIMAL_SERIALIZER = - new JsonSerializer<>() { - @Override - public void serialize(RegoDecimal value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - Double d = value.getValue(); - // If the double is a whole number that fits in a long, serialize as integer - if (d != null && TypeUtils.isWholeNumberInLongRange(d)) { - gen.writeNumber(d.longValue()); - } else { - gen.writeNumber(d); - } - } - }; - - private static final JsonSerializer REGO_BIG_INT_SERIALIZER = - new JsonSerializer<>() { - @Override - public void serialize(RegoBigInt value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeNumber(value.getValue()); - } - }; - - private static final JsonSerializer REGO_INT32_SERIALIZER = - new JsonSerializer<>() { - @Override - public void serialize(RegoInt32 value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeNumber(value.getValue()); - } - }; - - private static final JsonSerializer REGO_NULL_SERIALIZER = - new JsonSerializer<>() { - @Override - public void serialize(RegoNull value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeNull(); - } - }; + private static final ValueSerializer REGO_DECIMAL_SERIALIZER = + new ValueSerializer<>() { + @Override + public void serialize(RegoDecimal value, JsonGenerator gen, SerializationContext ctx) { + Double d = value.getValue(); + // If the double is a whole number that fits in a long, serialize as integer + if (d != null && TypeUtils.isWholeNumberInLongRange(d)) { + gen.writeNumber(d.longValue()); + } else { + gen.writeNumber(d); + } + } + }; - static { - // findAndRegisterModules picks up RegoValueModule (from opa-jackson) via SPI so - // RegoString/RegoArray/RegoObject etc. (de)serialize without annotations on the AST types. - JSON_MAPPER = new ObjectMapper().findAndRegisterModules(); + private static final ValueSerializer REGO_BIG_INT_SERIALIZER = + new ValueSerializer<>() { + @Override + public void serialize(RegoBigInt value, JsonGenerator gen, SerializationContext ctx) { + gen.writeNumber(value.getValue()); + } + }; + + private static final ValueSerializer REGO_INT32_SERIALIZER = + new ValueSerializer<>() { + @Override + public void serialize(RegoInt32 value, JsonGenerator gen, SerializationContext ctx) { + gen.writeNumber(value.getValue()); + } + }; + + private static final ValueSerializer REGO_NULL_SERIALIZER = + new ValueSerializer<>() { + @Override + public void serialize(RegoNull value, JsonGenerator gen, SerializationContext ctx) { + gen.writeNull(); + } + }; - // Register custom serializers for Rego numeric types (overrides RegoValueModule defaults). + static { SimpleModule module = new SimpleModule(); module.addSerializer(RegoDecimal.class, REGO_DECIMAL_SERIALIZER); module.addSerializer(RegoBigInt.class, REGO_BIG_INT_SERIALIZER); module.addSerializer(RegoInt32.class, REGO_INT32_SERIALIZER); - JSON_MAPPER.registerModule(module); + + // findAndAddModules picks up RegoValueModule (from opa-jackson) via SPI so + // RegoString/RegoArray/RegoObject etc. (de)serialize without annotations on the AST types. + JSON_MAPPER = JsonMapper.builder().findAndAddModules().addModule(module).build(); // YAML mapper needs all serializers including null handling SimpleModule yamlModule = new SimpleModule(); @@ -102,10 +94,11 @@ public void serialize(RegoNull value, JsonGenerator gen, SerializerProvider seri yamlModule.addSerializer(RegoNull.class, REGO_NULL_SERIALIZER); YAML_MAPPER = - new ObjectMapper( - YAMLFactory.builder().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER).build()) - .findAndRegisterModules(); - YAML_MAPPER.registerModule(yamlModule); + YAMLMapper.builder() + .disable(YAMLWriteFeature.WRITE_DOC_START_MARKER) + .findAndAddModules() + .addModule(yamlModule) + .build(); } @Override @@ -145,7 +138,7 @@ public RegoString marshal(EvaluationContext ctx, RegoValue[] args) { try { String json = JSON_MAPPER.writeValueAsString(input); return new RegoString(json); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new BuiltinError("json.marshal: " + e.getMessage()); } } @@ -164,7 +157,7 @@ public RegoString marshal_with_options(EvaluationContext ctx, RegoValue[] args) RegoObject options = getArg(args, 1, RegoObject.class); try { - ObjectMapper mapper = JSON_MAPPER.copy(); + ObjectWriter writer = JSON_MAPPER.writer(); // Check for indent option RegoValue indentValue = options.getProperty(new RegoString("indent")); @@ -176,20 +169,19 @@ public RegoString marshal_with_options(EvaluationContext ctx, RegoValue[] args) new DefaultIndenter(indent, DefaultIndenter.SYS_LF); printer.indentArraysWith(indenter); printer.indentObjectsWith(indenter); - mapper.setDefaultPrettyPrinter(printer); - mapper.enable(SerializationFeature.INDENT_OUTPUT); + writer = writer.with(printer).with(SerializationFeature.INDENT_OUTPUT); } // Check for prefix option (for pretty printing) RegoValue prefixValue = options.getProperty(new RegoString("prefix")); if (prefixValue instanceof RegoString) { // Prefix is typically used with indent for pretty printing - mapper.enable(SerializationFeature.INDENT_OUTPUT); + writer = writer.with(SerializationFeature.INDENT_OUTPUT); } - String json = mapper.writeValueAsString(input); + String json = writer.writeValueAsString(input); return new RegoString(json); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new BuiltinError("json.marshal_with_options: " + e.getMessage()); } } @@ -206,7 +198,7 @@ public RegoValue unmarshal(EvaluationContext ctx, RegoValue[] args) { try { Object parsed = JSON_MAPPER.readValue(jsonInput, Object.class); return convertToRegoValue(parsed); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new BuiltinError("json.unmarshal: " + e.getMessage()); } } @@ -224,7 +216,7 @@ public RegoBoolean is_valid(EvaluationContext ctx, RegoValue[] args) { try { JSON_MAPPER.readTree(jsonInput); return RegoBoolean.TRUE; - } catch (JsonProcessingException e) { + } catch (JacksonException e) { return RegoBoolean.FALSE; } } @@ -343,7 +335,7 @@ public RegoValue patch(EvaluationContext ctx, RegoValue[] args) { ArrayNode normalizedPatches = JSON_MAPPER.createArrayNode(); for (JsonNode patchOp : patchesNode) { if (patchOp.isObject()) { - ObjectNode normalizedOp = patchOp.deepCopy(); + ObjectNode normalizedOp = (ObjectNode) patchOp.deepCopy(); // Verify required fields exist - if not, return undefined (OPA behavior) if (!normalizedOp.has("op") || !normalizedOp.has("path")) { @@ -356,7 +348,7 @@ public RegoValue patch(EvaluationContext ctx, RegoValue[] args) { } // For add/replace/test operations, verify "value" field exists - String op = normalizedOp.get("op").asText(); + String op = normalizedOp.get("op").asString(); if (("add".equals(op) || "replace".equals(op) || "test".equals(op)) && !normalizedOp.has("value")) { return RegoUndefined.INSTANCE; @@ -369,6 +361,11 @@ public RegoValue patch(EvaluationContext ctx, RegoValue[] args) { } } + // Validate op type - return undefined for unknown ops (OPA behavior) + if (!isValidPatchOp(op)) { + return RegoUndefined.INSTANCE; + } + // Normalize "path" field JsonNode pathNode = normalizedOp.get("path"); if (pathNode != null) { @@ -404,25 +401,13 @@ public RegoValue patch(EvaluationContext ctx, RegoValue[] args) { } // Apply the patch - JsonPatch jsonPatch; - try { - jsonPatch = JsonPatch.fromJson(patchesNode); - } catch (InvalidTypeIdException e) { - // Invalid operation type (not one of: add, copy, move, remove, replace, test) - // Return undefined (OPA behavior) - return RegoUndefined.INSTANCE; - } - - if (jsonPatch == null) { - throw new BuiltinError("json.patch: failed to create JsonPatch"); - } - JsonNode patched; try { - patched = jsonPatch.apply(objectNode); - } catch (JsonPatchException e) { - // Return undefined on patch failure (OPA behavior) - // This happens when the patch operation is invalid (e.g., removing non-existent path) + patched = applyJsonPatch((ArrayNode) patchesNode, objectNode); + } catch (RuntimeException e) { + // Return undefined on patch failure (OPA behavior). + // This happens when the patch operation is invalid (e.g., removing non-existent path, + // index out of range, or test mismatch). return RegoUndefined.INSTANCE; } @@ -443,6 +428,194 @@ public RegoValue patch(EvaluationContext ctx, RegoValue[] args) { } } + private static boolean isValidPatchOp(String op) { + return "add".equals(op) + || "remove".equals(op) + || "replace".equals(op) + || "move".equals(op) + || "copy".equals(op) + || "test".equals(op); + } + + // RFC 6902 JSON Patch application. Each op mutates a deep copy of the source; on any failure + // (missing path, index out of range, test mismatch, unknown op), throws a RuntimeException + // which json.patch translates to RegoUndefined per OPA's contract. + private static JsonNode applyJsonPatch(ArrayNode patches, JsonNode source) { + JsonNode result = source.deepCopy(); + for (JsonNode op : patches) { + result = applyJsonPatchOp(result, op); + } + return result; + } + + private static JsonNode applyJsonPatchOp(JsonNode root, JsonNode op) { + String operation = op.get("op").asString(); + List path = parseJsonPointer(op.get("path").asString()); + switch (operation) { + case "add": + return performAdd(root, path, op.get("value").deepCopy()); + case "remove": + return performRemove(root, path); + case "replace": + return performReplace(root, path, op.get("value").deepCopy()); + case "test": + performTest(root, path, op.get("value")); + return root; + case "move": { + List from = parseJsonPointer(op.get("from").asString()); + if (isPrefix(from, path)) { + // RFC 6902 §4.4: from MUST NOT be a proper prefix of path. + throw new IllegalArgumentException("move: from is a prefix of path"); + } + JsonNode value = readAt(root, from); + root = performRemove(root, from); + return performAdd(root, path, value); + } + case "copy": { + List from = parseJsonPointer(op.get("from").asString()); + JsonNode value = readAt(root, from).deepCopy(); + return performAdd(root, path, value); + } + default: + throw new IllegalArgumentException("unknown op: " + operation); + } + } + + // RFC 6901 JSON Pointer: "" -> root (empty list); "/foo/0" -> ["foo", "0"]; + // "~1" decodes to "/", "~0" decodes to "~". + private static List parseJsonPointer(String pointer) { + if (pointer.isEmpty()) { + return new ArrayList<>(); + } + if (!pointer.startsWith("/")) { + throw new IllegalArgumentException("invalid JSON pointer: " + pointer); + } + String[] parts = pointer.substring(1).split("/", -1); + List tokens = new ArrayList<>(parts.length); + for (String part : parts) { + tokens.add(part.replace("~1", "/").replace("~0", "~")); + } + return tokens; + } + + private static JsonNode readAt(JsonNode root, List tokens) { + JsonNode current = root; + for (String token : tokens) { + if (current.isArray()) { + current = current.get(parseArrayIndex(token, current.size(), false)); + } else if (current.isObject()) { + if (!current.has(token)) { + throw new IllegalArgumentException("path does not exist: " + token); + } + current = current.get(token); + } else { + throw new IllegalArgumentException("cannot navigate into non-container at: " + token); + } + } + return current; + } + + private static JsonNode performAdd(JsonNode root, List tokens, JsonNode value) { + if (tokens.isEmpty()) { + // Replace the whole document. + return value; + } + JsonNode parent = readAt(root, tokens.subList(0, tokens.size() - 1)); + String last = tokens.get(tokens.size() - 1); + if (parent.isArray()) { + ArrayNode arr = (ArrayNode) parent; + int idx = "-".equals(last) ? arr.size() : parseArrayIndex(last, arr.size(), true); + if (idx == arr.size()) { + arr.add(value); + } else { + arr.insert(idx, value); + } + } else if (parent.isObject()) { + // RFC 6902 §4.1: existing member is replaced; new member is added. + ((ObjectNode) parent).set(last, value); + } else { + throw new IllegalArgumentException("add: parent is not a container"); + } + return root; + } + + private static JsonNode performRemove(JsonNode root, List tokens) { + if (tokens.isEmpty()) { + throw new IllegalArgumentException("remove: cannot remove root"); + } + JsonNode parent = readAt(root, tokens.subList(0, tokens.size() - 1)); + String last = tokens.get(tokens.size() - 1); + if (parent.isArray()) { + ArrayNode arr = (ArrayNode) parent; + arr.remove(parseArrayIndex(last, arr.size(), false)); + } else if (parent.isObject()) { + ObjectNode obj = (ObjectNode) parent; + if (!obj.has(last)) { + throw new IllegalArgumentException("remove: path does not exist: " + last); + } + obj.remove(last); + } else { + throw new IllegalArgumentException("remove: parent is not a container"); + } + return root; + } + + private static JsonNode performReplace(JsonNode root, List tokens, JsonNode value) { + if (tokens.isEmpty()) { + return value; + } + JsonNode parent = readAt(root, tokens.subList(0, tokens.size() - 1)); + String last = tokens.get(tokens.size() - 1); + if (parent.isArray()) { + ArrayNode arr = (ArrayNode) parent; + arr.set(parseArrayIndex(last, arr.size(), false), value); + } else if (parent.isObject()) { + ObjectNode obj = (ObjectNode) parent; + if (!obj.has(last)) { + throw new IllegalArgumentException("replace: path does not exist: " + last); + } + obj.set(last, value); + } else { + throw new IllegalArgumentException("replace: parent is not a container"); + } + return root; + } + + private static void performTest(JsonNode root, List tokens, JsonNode expected) { + JsonNode actual = readAt(root, tokens); + if (!actual.equals(expected)) { + throw new IllegalArgumentException("test: value mismatch"); + } + } + + // For "remove"/"replace"/"read", index must be in [0, size). For "add"/"insert", [0, size] + // (since size is "append at end"). + private static int parseArrayIndex(String token, int size, boolean inclusiveUpper) { + int idx; + try { + idx = Integer.parseInt(token); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid array index: " + token); + } + int limit = inclusiveUpper ? size : size - 1; + if (idx < 0 || idx > limit) { + throw new IllegalArgumentException("array index out of bounds: " + idx); + } + return idx; + } + + private static boolean isPrefix(List prefix, List path) { + if (prefix.size() > path.size()) { + return false; + } + for (int i = 0; i < prefix.size(); i++) { + if (!prefix.get(i).equals(path.get(i))) { + return false; + } + } + return true; + } + /** * Validates a JSON Pointer path according to RFC 6901. Specifically checks for invalid array * indices with leading zeros. @@ -515,7 +688,7 @@ private String normalizeAndResolveJsonPointerPath(JsonNode pathNode, JsonNode do boolean found = false; for (int i = 0; i < current.size(); i++) { JsonNode element = current.get(i); - if (element.isTextual() && element.asText().equals(unescaped)) { + if (element.isString() && element.asString().equals(unescaped)) { resolvedPath.append("/").append(i); current = element; found = true; @@ -545,8 +718,8 @@ private String normalizeAndResolveJsonPointerPath(JsonNode pathNode, JsonNode do * "/" and converts array paths to string format. */ private String normalizeJsonPointerPath(JsonNode pathNode) { - if (pathNode.isTextual()) { - String path = pathNode.asText(); + if (pathNode.isString()) { + String path = pathNode.asString(); if (path.isEmpty() || path.equals("/")) { return path; } @@ -556,20 +729,20 @@ private String normalizeJsonPointerPath(JsonNode pathNode) { StringBuilder sb = new StringBuilder(); for (JsonNode segment : pathNode) { sb.append("/"); - if (segment.isTextual()) { - String segmentStr = segment.asText(); + if (segment.isString()) { + String segmentStr = segment.asString(); // Escape special characters per RFC 6901 segmentStr = segmentStr.replace("~", "~0").replace("/", "~1"); sb.append(segmentStr); } else { // For numeric indices, just append as-is - sb.append(segment.asText()); + sb.append(segment.asString()); } } return sb.toString(); } else { // Fallback: convert to string - return "/" + pathNode.asText(); + return "/" + pathNode.asString(); } } @@ -612,9 +785,9 @@ public RegoValue match_schema(EvaluationContext ctx, RegoValue[] args) { } // Validate - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); - JsonSchema schema = factory.getSchema(schemaNode); - Set errors = schema.validate(documentNode); + SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); + Schema schema = registry.getSchema(schemaNode); + List errors = schema.validate(documentNode); // Build result array RegoArray result = new RegoArray(); @@ -624,12 +797,12 @@ public RegoValue match_schema(EvaluationContext ctx, RegoValue[] args) { } else { result.addValue(RegoBoolean.FALSE); RegoArray errorArray = new RegoArray(); - for (ValidationMessage error : errors) { + for (Error error : errors) { RegoObject errorObj = new RegoObject(); errorObj.setProp(new RegoString("message"), new RegoString(error.getMessage())); errorObj.setProp( new RegoString("path"), new RegoString(error.getEvaluationPath().toString())); - errorObj.setProp(new RegoString("type"), new RegoString(error.getType())); + errorObj.setProp(new RegoString("type"), new RegoString(error.getKeyword())); errorArray.addValue(errorObj); } result.addValue(errorArray); @@ -667,8 +840,8 @@ public RegoValue verify_schema(EvaluationContext ctx, RegoValue[] args) { } // Try to create a schema - if it succeeds, it's valid - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); - factory.getSchema(schemaNode); + SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); + registry.getSchema(schemaNode); // Build success result RegoArray result = new RegoArray(); @@ -701,7 +874,7 @@ public RegoString yamlMarshal(EvaluationContext ctx, RegoValue[] args) { try { String yaml = YAML_MAPPER.writeValueAsString(input); return new RegoString(yaml); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new BuiltinError("yaml.marshal: " + e.getMessage()); } } @@ -718,7 +891,7 @@ public RegoValue yamlUnmarshal(EvaluationContext ctx, RegoValue[] args) { try { Object parsed = YAML_MAPPER.readValue(yamlInput, Object.class); return convertToRegoValueFromYaml(parsed); - } catch (IOException e) { + } catch (JacksonException e) { throw new BuiltinError("yaml.unmarshal: " + e.getMessage()); } } @@ -851,7 +1024,7 @@ private Set extractPaths(RegoValue pathsArg, boolean sortedSets) { /** Filters a JsonNode to only include specified paths. */ private JsonNode filterNode(JsonNode node, Set paths) { if (node.isObject()) { - com.fasterxml.jackson.databind.node.ObjectNode result = JSON_MAPPER.createObjectNode(); + ObjectNode result = JSON_MAPPER.createObjectNode(); // Track array index mappings: key is the result array node, value is map of source->result // indices @@ -938,8 +1111,7 @@ private JsonNode filterNode(JsonNode node, Set paths) { boolean nextIsArray = (i + 1 < isArrayAtLevel.size()) && isArrayAtLevel.get(i + 1); if (resultNode.isObject()) { - com.fasterxml.jackson.databind.node.ObjectNode objNode = - (com.fasterxml.jackson.databind.node.ObjectNode) resultNode; + ObjectNode objNode = (ObjectNode) resultNode; if (!objNode.has(part)) { if (nextIsArray) { objNode.set(part, JSON_MAPPER.createArrayNode()); @@ -1061,4 +1233,4 @@ private JsonNode removeNodes(JsonNode node, Set paths) { return result; } -} \ No newline at end of file +} diff --git a/opa-builtins/opa-builtins-token/build.gradle.kts b/opa-builtins/opa-builtins-token/build.gradle.kts index da30155c..71b4c846 100644 --- a/opa-builtins/opa-builtins-token/build.gradle.kts +++ b/opa-builtins/opa-builtins-token/build.gradle.kts @@ -11,7 +11,8 @@ dependencies { implementation("com.nimbusds:nimbus-jose-jwt:10.9.1") implementation("org.bouncycastle:bcpkix-jdk18on:1.82") - implementation("com.fasterxml.jackson.core:jackson-databind:2.21.3") + implementation(platform("tools.jackson:jackson-bom:3.2.0")) + implementation("tools.jackson.core:jackson-databind") // RegoValueModule provides Jackson (de)serialization for RegoObject/RegoArray/etc. // Discovered automatically via Jackson's findAndRegisterModules() SPI. runtimeOnly(project(":opa-jackson")) diff --git a/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java b/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java index 20a3a22f..9e01c23f 100644 --- a/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java +++ b/opa-builtins/opa-builtins-token/src/main/java/io/github/open_policy_agent/opa/ast/builtin/impls/TokenBuiltins.java @@ -1,11 +1,17 @@ package io.github.open_policy_agent.opa.ast.builtin.impls; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import static io.github.open_policy_agent.opa.ast.builtin.impls.utils.ArgHelper.getArg; + import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.*; import com.nimbusds.jose.jwk.*; import com.nimbusds.jwt.*; +import io.github.open_policy_agent.opa.ast.builtin.BuiltinError; +import io.github.open_policy_agent.opa.ast.builtin.BuiltinProvider; +import io.github.open_policy_agent.opa.ast.builtin.OpaBuiltin; +import io.github.open_policy_agent.opa.ast.builtin.OpaType; +import io.github.open_policy_agent.opa.ast.types.*; +import io.github.open_policy_agent.opa.rego.EvaluationContext; import java.io.IOException; import java.io.StringReader; import java.security.PrivateKey; @@ -21,14 +27,10 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import io.github.open_policy_agent.opa.ast.builtin.BuiltinError; -import io.github.open_policy_agent.opa.ast.builtin.BuiltinProvider; -import io.github.open_policy_agent.opa.ast.builtin.OpaBuiltin; -import io.github.open_policy_agent.opa.ast.builtin.OpaType; -import io.github.open_policy_agent.opa.ast.types.*; -import io.github.open_policy_agent.opa.rego.EvaluationContext; - -import static io.github.open_policy_agent.opa.ast.builtin.impls.utils.ArgHelper.getArg; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; public class TokenBuiltins implements BuiltinProvider { @@ -43,12 +45,14 @@ public class TokenBuiltins implements BuiltinProvider { private static final RegoObject BLANK_OBJECT = new RegoObject(); // Auto-register RegoValueModule (and any other Jackson modules on the classpath) via SPI so // RegoObject (de)serialization works without the AST types carrying Jackson annotations. - private static final ObjectMapper JSON_MAPPER = new ObjectMapper().findAndRegisterModules(); + private static final ObjectMapper JSON_MAPPER = + JsonMapper.builder() + .findAndAddModules() + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .build(); static { Security.addProvider(new BouncyCastleProvider()); - JSON_MAPPER.enable( - com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); } @Override @@ -133,7 +137,7 @@ private static String getValidPayload(JWSHeader header, RegoString payloadJson) try { JSON_MAPPER.readTree(payload); - } catch (IOException e) { + } catch (JacksonException e) { throw new BuiltinError("io.jwt.encode_sign_raw: type is JWT but payload is not JSON"); } } else { @@ -579,11 +583,11 @@ private void validateExpAndNbfClaims(String payloadJson) { } try { - com.fasterxml.jackson.databind.JsonNode root = JSON_MAPPER.readTree(payloadJson); + tools.jackson.databind.JsonNode root = JSON_MAPPER.readTree(payloadJson); // Check exp claim if (root.has("exp")) { - com.fasterxml.jackson.databind.JsonNode expNode = root.get("exp"); + tools.jackson.databind.JsonNode expNode = root.get("exp"); if (expNode.isNull()) { throw new BuiltinError("exp value must be a number"); } @@ -594,7 +598,7 @@ private void validateExpAndNbfClaims(String payloadJson) { // Check nbf claim if (root.has("nbf")) { - com.fasterxml.jackson.databind.JsonNode nbfNode = root.get("nbf"); + tools.jackson.databind.JsonNode nbfNode = root.get("nbf"); if (nbfNode.isNull()) { throw new BuiltinError("nbf value must be a number"); } @@ -602,7 +606,7 @@ private void validateExpAndNbfClaims(String payloadJson) { throw new BuiltinError("nbf value must be a number"); } } - } catch (JsonProcessingException ignore) { + } catch (JacksonException ignore) { // If it's not valid JSON, we'll let it through - it will be treated as a string payload } } @@ -638,7 +642,7 @@ public com.nimbusds.jose.jca.JCAContext getJCAContext() { private JWSSigner createEdDSASigner(String keyJson) { try { - com.fasterxml.jackson.databind.JsonNode jwkNode = JSON_MAPPER.readTree(keyJson); + tools.jackson.databind.JsonNode jwkNode = JSON_MAPPER.readTree(keyJson); if (!jwkNode.has("d")) { throw new BuiltinError("EdDSA key must include private key (d parameter)"); } diff --git a/opa-evaluator/build.gradle.kts b/opa-evaluator/build.gradle.kts index ef7eb487..9e0b2007 100644 --- a/opa-evaluator/build.gradle.kts +++ b/opa-evaluator/build.gradle.kts @@ -13,7 +13,8 @@ dependencies { // modules through SPIs (see Engine javadoc); opa-jackson is one such implementation, used here // for testing. testImplementation(project(":opa-jackson")) - testImplementation("com.fasterxml.jackson.core:jackson-databind:2.21.3") + testImplementation(platform("tools.jackson:jackson-bom:3.2.0")) + testImplementation("tools.jackson.core:jackson-databind") testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2") testImplementation("org.json:json:20260522") diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/bundle/FileSystemBundleLoaderTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/bundle/FileSystemBundleLoaderTest.java index 6e59227e..01f3ff44 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/bundle/FileSystemBundleLoaderTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/bundle/FileSystemBundleLoaderTest.java @@ -4,8 +4,9 @@ import io.github.open_policy_agent.opa.rego.Engine; import io.github.open_policy_agent.opa.storage.InMem; import io.github.open_policy_agent.opa.storage.Store; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -21,7 +22,7 @@ class FileSystemBundleLoaderTest { - private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + private static final ObjectMapper MAPPER = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private Path testResource(String name) { return Paths.get( diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/integration/NdBuiltinCacheTrackingTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/integration/NdBuiltinCacheTrackingTest.java index 7db0428a..4f5fc2bf 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/integration/NdBuiltinCacheTrackingTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/integration/NdBuiltinCacheTrackingTest.java @@ -2,8 +2,9 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -124,7 +125,7 @@ public void testJsonSerialization() throws Exception { Map> ndCache = ctx.getNdCacheValues(); // Serialize to JSON (simulating what DecisionLogPlugin does) - ObjectMapper mapper = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + ObjectMapper mapper = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); JsonNode cacheNode = mapper.valueToTree(ndCache); // This test verifies that the cache values can be serialized diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/ComplianceTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/ComplianceTest.java index 2815f3df..91822ac6 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/ComplianceTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/ComplianceTest.java @@ -20,11 +20,12 @@ import io.github.open_policy_agent.opa.rego.EvaluationContext; import io.github.open_policy_agent.opa.tracing.BufferedQueryTracer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.NullNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.NullNode; +import tools.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -118,18 +119,14 @@ public static Stream getComplianceTestData() throws IOException { .filter(path -> path.toString().endsWith(".json")) .map(Path::toFile) .flatMap(f -> { - try { - ObjectMapper mapper = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); - JsonNode root = mapper.readTree(f); - List cases = new ArrayList<>(); - root.get("cases").forEach(cases::add); - return cases.stream() - .map(c -> new Object[]{ - c.get("note").asText("unknown"), c - }); - } catch (IOException e) { - throw new RuntimeException(e); - } + ObjectMapper mapper = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); + JsonNode root = mapper.readTree(f); + List cases = new ArrayList<>(); + root.get("cases").forEach(cases::add); + return cases.stream() + .map(c -> new Object[]{ + c.get("note").asString("unknown"), c + }); }); } @@ -318,18 +315,16 @@ private static RegoValue jsonNodeToRegoValue( // When treatArraysAsSets is true, recursively process object values if (treatArraysAsSets) { RegoObject obj = new RegoObject(); - node.fields() - .forEachRemaining( - entry -> { - try { - RegoValue key = new RegoString(entry.getKey()); - RegoValue value = - jsonNodeToRegoValue(entry.getValue(), mapper, treatArraysAsSets); - obj.setProp(key, value); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + for (Map.Entry entry : node.properties()) { + try { + RegoValue key = new RegoString(entry.getKey()); + RegoValue value = + jsonNodeToRegoValue(entry.getValue(), mapper, treatArraysAsSets); + obj.setProp(key, value); + } catch (IOException e) { + throw new RuntimeException(e); + } + } return obj; } else { return mapper.treeToValue(node, RegoObject.class); @@ -344,7 +339,7 @@ private static RegoValue jsonNodeToRegoValue( @MethodSource("getComplianceTestData") public void testEvaluate(String caseName, JsonNode root) { try { - ObjectMapper mapper = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + ObjectMapper mapper = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); if (root.has("skip") && root.get("skip").asBoolean()) { System.out.println("skipping: " + caseName); @@ -513,9 +508,7 @@ private static JsonNode deepSortArrays(JsonNode node) { } else if (node.isObject()) { ObjectNode obj = (ObjectNode) node; ObjectNode result = obj.objectNode(); - Iterator> fields = obj.fields(); - while (fields.hasNext()) { - Map.Entry field = fields.next(); + for (Map.Entry field : obj.properties()) { result.set(field.getKey(), deepSortArrays(field.getValue())); } return result; diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/EvaluatorTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/EvaluatorTest.java index ce588014..0dd6a500 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/EvaluatorTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/ir/EvaluatorTest.java @@ -12,7 +12,8 @@ import io.github.open_policy_agent.opa.ir.policy.StringConst; import io.github.open_policy_agent.opa.rego.EvaluationContext; import io.github.open_policy_agent.opa.tracing.BufferedQueryTracer; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.Test; import java.io.BufferedReader; @@ -32,7 +33,7 @@ class EvaluatorTest { private static final PolicyReader policyReader = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); - private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + private static final ObjectMapper objectMapper = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); @Test void evaluate_BreakStmt_IndexZero() throws IOException { diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/metrics/MetricsIntegrationTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/metrics/MetricsIntegrationTest.java index 78cefe5c..86ff2fba 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/metrics/MetricsIntegrationTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/metrics/MetricsIntegrationTest.java @@ -5,8 +5,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -35,7 +36,7 @@ class MetricsIntegrationTest { private static final ObjectMapper MAPPER = - new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private static final PolicyReader POLICY_READER = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); private static final String ENTRYPOINT = "authz/allow"; diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/profiling/SimpleStatementProfilerIntegrationTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/profiling/SimpleStatementProfilerIntegrationTest.java index 32279d0e..20a6bf69 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/profiling/SimpleStatementProfilerIntegrationTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/profiling/SimpleStatementProfilerIntegrationTest.java @@ -5,7 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -32,7 +33,7 @@ class SimpleStatementProfilerIntegrationTest { private static final ObjectMapper MAPPER = - new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private static final PolicyReader POLICY_READER = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); private static final String ENTRYPOINT = "authz/allow"; diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/CrossBundleDataTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/CrossBundleDataTest.java index 255cf86a..dadcecea 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/CrossBundleDataTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/CrossBundleDataTest.java @@ -2,8 +2,9 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -34,7 +35,7 @@ */ class CrossBundleDataTest { - private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + private static final ObjectMapper MAPPER = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private static final PolicyReader POLICY_READER = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); private static final String ENTRYPOINT = "authz/allow"; diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineEvaluateTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineEvaluateTest.java index 95fe00ba..5bafff1f 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineEvaluateTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineEvaluateTest.java @@ -4,8 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -45,7 +46,7 @@ */ class EngineEvaluateTest { - private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + private static final ObjectMapper MAPPER = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private static final PolicyReader POLICY_READER = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); private static final String ENTRYPOINT = "authz/allow"; diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineHotReloadTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineHotReloadTest.java index ea35a33a..c6d6b8d8 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineHotReloadTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineHotReloadTest.java @@ -6,8 +6,9 @@ import io.github.open_policy_agent.opa.ir.policy.Policy; import io.github.open_policy_agent.opa.storage.InMem; import io.github.open_policy_agent.opa.storage.Store; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -29,7 +30,7 @@ */ class EngineHotReloadTest { - private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + private static final ObjectMapper MAPPER = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private static final PolicyReader POLICY_READER = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); private static final String ENTRYPOINT = "authz/allow"; diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineTest.java index b40d1e5b..9f9a3b57 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/EngineTest.java @@ -12,8 +12,9 @@ import io.github.open_policy_agent.opa.metrics.NoOpMetrics; import io.github.open_policy_agent.opa.storage.InMem; import io.github.open_policy_agent.opa.storage.Store; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import io.github.open_policy_agent.opa.tracing.DurationProfiler; import org.junit.jupiter.api.Test; @@ -30,7 +31,7 @@ class EngineTest { private static final PolicyReader policyReader = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); - private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + private static final ObjectMapper objectMapper = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); @Test void engine_builder_requiresStore() { diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/JsonNodeBridge.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/JsonNodeBridge.java index 8eda4d66..63488458 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/JsonNodeBridge.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/rego/JsonNodeBridge.java @@ -1,7 +1,8 @@ package io.github.open_policy_agent.opa.rego; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import io.github.open_policy_agent.opa.jackson.RegoValueModule; import java.util.ArrayList; import java.util.List; @@ -14,7 +15,7 @@ */ public final class JsonNodeBridge { private static final ObjectMapper MAPPER = - new ObjectMapper().registerModule(new RegoValueModule()); + JsonMapper.builder().addModule(new RegoValueModule()).build(); private JsonNodeBridge() {} diff --git a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/tracing/DurationProfilerIntegrationTest.java b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/tracing/DurationProfilerIntegrationTest.java index 7e217f93..9bd553d3 100644 --- a/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/tracing/DurationProfilerIntegrationTest.java +++ b/opa-evaluator/src/test/java/io/github/open_policy_agent/opa/tracing/DurationProfilerIntegrationTest.java @@ -5,7 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -34,7 +35,7 @@ class DurationProfilerIntegrationTest { private static final ObjectMapper MAPPER = - new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private static final PolicyReader POLICY_READER = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); private static final String ENTRYPOINT = "authz/allow"; diff --git a/opa-jackson/build.gradle.kts b/opa-jackson/build.gradle.kts index 90eab045..9967e353 100644 --- a/opa-jackson/build.gradle.kts +++ b/opa-jackson/build.gradle.kts @@ -8,9 +8,9 @@ repositories { dependencies { api(project(":opa-evaluator")) - implementation("com.fasterxml.jackson.core:jackson-databind:2.21.3") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.0") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0") + implementation(platform("tools.jackson:jackson-bom:3.2.0")) + implementation("tools.jackson.core:jackson-databind") + implementation("tools.jackson.dataformat:jackson-dataformat-yaml") testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") testImplementation("org.assertj:assertj-core:3.27.7") diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/IrModule.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/IrModule.java index d3b34ea6..410ce546 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/IrModule.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/IrModule.java @@ -3,16 +3,16 @@ import io.github.open_policy_agent.opa.ir.Operand; import io.github.open_policy_agent.opa.ir.policy.types.Type; import io.github.open_policy_agent.opa.ir.stmts.Stmt; -import com.fasterxml.jackson.databind.module.SimpleModule; +import tools.jackson.databind.module.SimpleModule; /** * Jackson {@link SimpleModule} that registers the OPA IR deserializers. * - *

Register this module on any {@link com.fasterxml.jackson.databind.ObjectMapper} that will + *

Register this module on any {@link tools.jackson.databind.ObjectMapper} that will * parse OPA IR policy documents: * *

{@code
- * ObjectMapper mapper = new ObjectMapper().registerModule(new IrModule());
+ * JsonMapper mapper = JsonMapper.builder().addModule(new IrModule()).build();
  * }
*/ public class IrModule extends SimpleModule { @@ -23,4 +23,4 @@ public IrModule() { addDeserializer(Stmt.class, new StmtDeserializer()); addDeserializer(Type.class, new TypeDeserializer()); } -} \ No newline at end of file +} diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonBundleParser.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonBundleParser.java index 73d72aba..f2d7f133 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonBundleParser.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonBundleParser.java @@ -1,13 +1,13 @@ package io.github.open_policy_agent.opa.jackson; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.github.open_policy_agent.opa.ast.types.RegoObject; import io.github.open_policy_agent.opa.bundle.BundleParser; - import java.io.IOException; import java.io.InputStream; import java.util.Map; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; /** * Jackson-backed {@link BundleParser}. @@ -18,9 +18,8 @@ public class JacksonBundleParser implements BundleParser { private static final ObjectMapper MAPPER = - new ObjectMapper().registerModule(new RegoValueModule()); - private static final TypeReference> MAP_TYPE = - new TypeReference<>() {}; + JsonMapper.builder().addModule(new RegoValueModule()).build(); + private static final TypeReference> MAP_TYPE = new TypeReference<>() {}; @Override public RegoObject parseData(InputStream in) throws IOException { diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonCapabilities.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonCapabilities.java index 04bf6b00..51d8de62 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonCapabilities.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonCapabilities.java @@ -1,9 +1,9 @@ package io.github.open_policy_agent.opa.jackson; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; import io.github.open_policy_agent.opa.rego.Capabilities; -import java.io.IOException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; /** * Jackson-backed JSON IO for {@link Capabilities}. @@ -14,17 +14,20 @@ public final class JacksonCapabilities { private static final ObjectMapper MAPPER = - new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); + JsonMapper.builder() + .changeDefaultPropertyInclusion( + incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) + .build(); private JacksonCapabilities() {} /** Parse a {@link Capabilities} from a JSON string. */ - public static Capabilities fromJson(String json) throws IOException { + public static Capabilities fromJson(String json) { return MAPPER.readValue(json, Capabilities.class); } /** Serialize a {@link Capabilities} to a pretty-printed JSON string. */ - public static String toJson(Capabilities capabilities) throws IOException { + public static String toJson(Capabilities capabilities) { return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(capabilities); } } diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonPolicyReader.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonPolicyReader.java index 5f8b468b..22b5f315 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonPolicyReader.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/JacksonPolicyReader.java @@ -2,14 +2,13 @@ import io.github.open_policy_agent.opa.ir.PolicyReader; import io.github.open_policy_agent.opa.ir.policy.Policy; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - import java.io.IOException; import java.io.InputStream; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.PropertyNamingStrategies; +import tools.jackson.databind.json.JsonMapper; /** * Jackson-based {@link PolicyReader} implementation. @@ -23,18 +22,18 @@ public class JacksonPolicyReader implements PolicyReader { private static final ObjectMapper MAPPER = - new ObjectMapper() - .registerModule(new IrModule()) - .registerModule(new JavaTimeModule()) + JsonMapper.builder() + .addModule(new IrModule()) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // OPA IR uses "Index" (capitalized) for MakeNumberRefStmt.index — accept // case-insensitive property names so the Java field can stay conventional. .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) // OPA IR uses snake_case for fields like Static.builtin_funcs. - .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .build(); @Override public Policy read(InputStream in) throws IOException { return MAPPER.readValue(in, Policy.class); } -} \ No newline at end of file +} diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/MetricsModule.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/MetricsModule.java index 2404f8a7..a26c2e7d 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/MetricsModule.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/MetricsModule.java @@ -1,11 +1,10 @@ package io.github.open_policy_agent.opa.jackson; import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; import io.github.open_policy_agent.opa.metrics.Metrics; import io.github.open_policy_agent.opa.metrics.SimpleMetrics; import java.time.Duration; +import tools.jackson.databind.module.SimpleModule; /** * Jackson {@link SimpleModule} that adds {@code @JsonValue} behavior to {@link SimpleMetrics}'s inner {@code Timer}. @@ -18,7 +17,7 @@ *

Usage: * *

{@code
- * ObjectMapper mapper = new ObjectMapper().registerModule(new MetricsModule());
+ * JsonMapper mapper = JsonMapper.builder().addModule(new MetricsModule()).build();
  * String json = mapper.writeValueAsString(simpleMetrics.timer("foo"));
  * }
*/ diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReport.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReport.java index 29aa1e22..7d32138a 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReport.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReport.java @@ -6,9 +6,9 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; import io.github.open_policy_agent.opa.tracing.CoverageProfiler; /** diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OperandDeserializer.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OperandDeserializer.java index b12965a4..a2fe7d45 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OperandDeserializer.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/OperandDeserializer.java @@ -1,22 +1,20 @@ package io.github.open_policy_agent.opa.jackson; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import io.github.open_policy_agent.opa.ir.Operand; import io.github.open_policy_agent.opa.ir.vals.BoolVal; import io.github.open_policy_agent.opa.ir.vals.LocalVal; import io.github.open_policy_agent.opa.ir.vals.StringIndexVal; import io.github.open_policy_agent.opa.ir.vals.Val; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; -import java.io.IOException; import java.util.HashMap; import java.util.Map; -class OperandDeserializer extends JsonDeserializer { +class OperandDeserializer extends ValueDeserializer { private static final Map> VAL_REGISTRY = new HashMap>() { { @@ -27,10 +25,8 @@ class OperandDeserializer extends JsonDeserializer { }; @Override - public Operand deserialize(JsonParser jp, DeserializationContext ctx) - throws IOException, JsonProcessingException { - ObjectMapper mapper = (ObjectMapper) jp.getCodec(); - JsonNode node = mapper.readTree(jp); + public Operand deserialize(JsonParser jp, DeserializationContext ctx) { + JsonNode node = ctx.readTree(jp); JsonNode opNode = node.get("type"); if (opNode == null) { return null; @@ -39,9 +35,9 @@ public Operand deserialize(JsonParser jp, DeserializationContext ctx) String opValType = opNode.asText(); Class opClass = VAL_REGISTRY.get(opValType); if (opClass == null) { - throw new IOException("unknown val type: " + opValType); + throw DatabindException.from(jp, "unknown val type: " + opValType); } - return new Operand(mapper.treeToValue(node, opClass)); + return new Operand(ctx.readTreeAsValue(node, opClass)); } -} \ No newline at end of file +} diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/RegoValueModule.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/RegoValueModule.java index 640bc1e6..98071bf0 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/RegoValueModule.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/RegoValueModule.java @@ -1,14 +1,5 @@ package io.github.open_policy_agent.opa.jackson; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; import io.github.open_policy_agent.opa.ast.types.RegoArray; import io.github.open_policy_agent.opa.ast.types.RegoBigInt; import io.github.open_policy_agent.opa.ast.types.RegoBoolean; @@ -20,19 +11,26 @@ import io.github.open_policy_agent.opa.ast.types.RegoSet; import io.github.open_policy_agent.opa.ast.types.RegoString; import io.github.open_policy_agent.opa.ast.types.RegoValue; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.module.SimpleModule; -import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Iterator; import java.util.Map; import java.util.TreeMap; /** - * Jackson {@link SimpleModule} that teaches an {@link ObjectMapper} how to (de)serialize the OPA - * Rego AST value types. Replaces the in-source {@code @JsonValue}/{@code @JsonAnySetter} - * annotations the AST types previously carried, so the evaluator module has no Jackson - * dependency. + * Jackson {@link SimpleModule} that teaches an {@link tools.jackson.databind.ObjectMapper} + * how to (de)serialize the OPA Rego AST value types. Replaces the in-source {@code @JsonValue}/ + * {@code @JsonAnySetter} annotations the AST types previously carried, so the evaluator module + * has no Jackson dependency. * *

Only a {@link RegoObject} deserializer is registered. Reading JSON directly into a typed * Rego value (i.e. {@code mapper.readValue(json, T.class)}) is only used with {@link RegoObject} @@ -42,7 +40,7 @@ *

Usage: * *

{@code
- * ObjectMapper mapper = new ObjectMapper().registerModule(new RegoValueModule());
+ * JsonMapper mapper = JsonMapper.builder().addModule(new RegoValueModule()).build();
  * String json = mapper.writeValueAsString(regoValue);
  * RegoObject obj = mapper.readValue(json, RegoObject.class);
  * }
@@ -63,73 +61,73 @@ public RegoValueModule() { addDeserializer(RegoObject.class, new RegoObjectDeserializer()); } - private static final class RegoStringSerializer extends JsonSerializer { + private static final class RegoStringSerializer extends ValueSerializer { @Override - public void serialize(RegoString v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoString v, JsonGenerator g, SerializationContext p) { g.writeString(v.getValue()); } } - private static final class RegoInt32Serializer extends JsonSerializer { + private static final class RegoInt32Serializer extends ValueSerializer { @Override - public void serialize(RegoInt32 v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoInt32 v, JsonGenerator g, SerializationContext p) { g.writeNumber(v.getValue()); } } - private static final class RegoBigIntSerializer extends JsonSerializer { + private static final class RegoBigIntSerializer extends ValueSerializer { @Override - public void serialize(RegoBigInt v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoBigInt v, JsonGenerator g, SerializationContext p) { g.writeNumber(v.getValue()); } } - private static final class RegoDecimalSerializer extends JsonSerializer { + private static final class RegoDecimalSerializer extends ValueSerializer { @Override - public void serialize(RegoDecimal v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoDecimal v, JsonGenerator g, SerializationContext p) { g.writeNumber(v.getValue()); } } - private static final class RegoBooleanSerializer extends JsonSerializer { + private static final class RegoBooleanSerializer extends ValueSerializer { @Override - public void serialize(RegoBoolean v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoBoolean v, JsonGenerator g, SerializationContext p) { g.writeBoolean(v.getValue()); } } - private static final class RegoNullSerializer extends JsonSerializer { + private static final class RegoNullSerializer extends ValueSerializer { @Override - public void serialize(RegoNull v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoNull v, JsonGenerator g, SerializationContext p) { g.writeNull(); } } - private static final class RegoArraySerializer extends JsonSerializer { + private static final class RegoArraySerializer extends ValueSerializer { @Override - public void serialize(RegoArray v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoArray v, JsonGenerator g, SerializationContext p) { g.writeStartArray(); for (RegoValue item : v.getValues()) { - p.defaultSerializeValue(item, g); + p.writeValue(g, item); } g.writeEndArray(); } } - private static final class RegoSetSerializer extends JsonSerializer { + private static final class RegoSetSerializer extends ValueSerializer { @Override - public void serialize(RegoSet v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoSet v, JsonGenerator g, SerializationContext p) { g.writeStartArray(); for (RegoValue item : v.getValue()) { - p.defaultSerializeValue(item, g); + p.writeValue(g, item); } g.writeEndArray(); } } - private static final class RegoObjectSerializer extends JsonSerializer { + private static final class RegoObjectSerializer extends ValueSerializer { @Override - public void serialize(RegoObject v, JsonGenerator g, SerializerProvider p) throws IOException { + public void serialize(RegoObject v, JsonGenerator g, SerializationContext p) { // OPA (Go) emits sorted keys; preserve that for round-trip fidelity. Map sorted = new TreeMap<>(); for (Map.Entry entry : v.getProperties().entrySet()) { @@ -137,8 +135,8 @@ public void serialize(RegoObject v, JsonGenerator g, SerializerProvider p) throw } g.writeStartObject(); for (Map.Entry entry : sorted.entrySet()) { - g.writeFieldName(entry.getKey()); - p.defaultSerializeValue(entry.getValue(), g); + g.writeName(entry.getKey()); + p.writeValue(g, entry.getValue()); } g.writeEndObject(); } @@ -154,23 +152,21 @@ private static String keyToString(RegoValue key) { } } - private static final class RegoObjectDeserializer extends JsonDeserializer { + private static final class RegoObjectDeserializer extends ValueDeserializer { @Override - public RegoObject deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { - JsonNode root = jp.getCodec().readTree(jp); + public RegoObject deserialize(JsonParser jp, DeserializationContext ctx) { + JsonNode root = ctx.readTree(jp); if (!root.isObject()) { - throw new IOException("expected JSON object for RegoObject, got " + root.getNodeType()); + throw DatabindException.from(jp, "expected JSON object for RegoObject, got " + root.getNodeType()); } RegoObject obj = new RegoObject(); - Iterator> fields = root.fields(); - while (fields.hasNext()) { - Map.Entry field = fields.next(); - obj.setProp(new RegoString(field.getKey()), convertNode(field.getValue())); + for (Map.Entry field : root.properties()) { + obj.setProp(new RegoString(field.getKey()), convertNode(jp, field.getValue())); } return obj; } - private static RegoValue convertNode(JsonNode node) throws IOException { + private static RegoValue convertNode(JsonParser jp, JsonNode node) { if (node == null || node.isNull()) { return RegoNull.INSTANCE; } @@ -198,20 +194,18 @@ private static RegoValue convertNode(JsonNode node) throws IOException { if (node.isArray()) { RegoArray arr = new RegoArray(); for (JsonNode element : node) { - arr.addValue(convertNode(element)); + arr.addValue(convertNode(jp, element)); } return arr; } if (node.isObject()) { RegoObject obj = new RegoObject(); - Iterator> fields = node.fields(); - while (fields.hasNext()) { - Map.Entry field = fields.next(); - obj.setProp(new RegoString(field.getKey()), convertNode(field.getValue())); + for (Map.Entry field : node.properties()) { + obj.setProp(new RegoString(field.getKey()), convertNode(jp, field.getValue())); } return obj; } - throw new IOException("unsupported JSON node type: " + node.getNodeType()); + throw DatabindException.from(jp, "unsupported JSON node type: " + node.getNodeType()); } } } diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/StmtDeserializer.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/StmtDeserializer.java index e886bcd8..1b5c1507 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/StmtDeserializer.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/StmtDeserializer.java @@ -1,13 +1,5 @@ package io.github.open_policy_agent.opa.jackson; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; import io.github.open_policy_agent.opa.ir.stmts.ArrayAppendStmt; import io.github.open_policy_agent.opa.ir.stmts.AssignIntStmt; import io.github.open_policy_agent.opa.ir.stmts.AssignVarOnceStmt; @@ -43,13 +35,16 @@ import io.github.open_policy_agent.opa.ir.stmts.SetAddStmt; import io.github.open_policy_agent.opa.ir.stmts.Stmt; import io.github.open_policy_agent.opa.ir.stmts.WithStmt; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; -import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -class StmtDeserializer extends JsonDeserializer { +class StmtDeserializer extends ValueDeserializer { // Maps JSON type strings to statement classes. // String values match the OPA IR spec and the STMT_TYPE enum's typeName values. private static final Map> STMT_REGISTRY = @@ -92,16 +87,9 @@ class StmtDeserializer extends JsonDeserializer { } }; - // Cache of bean deserializers, built directly via the factory so they bypass - // this StmtDeserializer (which is registered for the Stmt interface and would - // otherwise be re-entered for any Stmt subclass). - private final Map, JsonDeserializer> beanDeserializerCache = - new ConcurrentHashMap<>(); - @Override - public Stmt deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { - ObjectMapper mapper = (ObjectMapper) jp.getCodec(); - JsonNode root = mapper.readTree(jp); + public Stmt deserialize(JsonParser jp, DeserializationContext ctx) { + JsonNode root = ctx.readTree(jp); JsonNode typeNode = root.get("type"); if (typeNode == null) { return null; @@ -110,18 +98,15 @@ public Stmt deserialize(JsonParser jp, DeserializationContext ctx) throws IOExce String stmtType = typeNode.asText(); Class stmtClass = STMT_REGISTRY.get(stmtType); if (stmtClass == null) { - throw new IOException("unknown stmt type: " + stmtType); + throw DatabindException.from(jp, "unknown stmt type: " + stmtType); } JsonNode stmtNode = root.get("stmt"); if (stmtNode == null) { - throw new IOException("missing stmt field for stmt: " + stmtType); + throw DatabindException.from(jp, "missing stmt field for stmt: " + stmtType); } - JsonDeserializer beanDeserializer = beanDeserializerFor(stmtClass, ctx); - JsonParser nodeParser = stmtNode.traverse(mapper); - nodeParser.nextToken(); - Stmt stmt = (Stmt) beanDeserializer.deserialize(nodeParser, ctx); + Stmt stmt = ctx.readTreeAsValue(stmtNode, stmtClass); JsonNode file = stmtNode.get("file"); JsonNode row = stmtNode.get("row"); @@ -133,21 +118,4 @@ public Stmt deserialize(JsonParser jp, DeserializationContext ctx) throws IOExce return stmt; } - - private JsonDeserializer beanDeserializerFor( - Class stmtClass, DeserializationContext ctx) throws IOException { - JsonDeserializer cached = beanDeserializerCache.get(stmtClass); - if (cached != null) { - return cached; - } - JavaType javaType = ctx.constructType(stmtClass); - BeanDescription beanDesc = ctx.getConfig().introspect(javaType); - JsonDeserializer beanDeserializer = - ctx.getFactory().createBeanDeserializer(ctx, javaType, beanDesc); - if (beanDeserializer instanceof ResolvableDeserializer) { - ((ResolvableDeserializer) beanDeserializer).resolve(ctx); - } - beanDeserializerCache.put(stmtClass, beanDeserializer); - return beanDeserializer; - } } diff --git a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/TypeDeserializer.java b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/TypeDeserializer.java index 5cc64bbd..582d52bb 100644 --- a/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/TypeDeserializer.java +++ b/opa-jackson/src/main/java/io/github/open_policy_agent/opa/jackson/TypeDeserializer.java @@ -1,13 +1,5 @@ package io.github.open_policy_agent.opa.jackson; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; import io.github.open_policy_agent.opa.ir.policy.types.AnyType; import io.github.open_policy_agent.opa.ir.policy.types.ArrayType; import io.github.open_policy_agent.opa.ir.policy.types.BooleanType; @@ -18,13 +10,16 @@ import io.github.open_policy_agent.opa.ir.policy.types.SetType; import io.github.open_policy_agent.opa.ir.policy.types.StringType; import io.github.open_policy_agent.opa.ir.policy.types.Type; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; -import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -class TypeDeserializer extends JsonDeserializer { +class TypeDeserializer extends ValueDeserializer { // Type marker strings match the OPA IR spec typeMarker constants. private static final Map> TYPE_REGISTRY = new HashMap<>() { @@ -41,16 +36,9 @@ class TypeDeserializer extends JsonDeserializer { } }; - // Cache of bean deserializers, built directly via the factory so they bypass - // this TypeDeserializer (which is registered for the Type interface and would - // otherwise be re-entered for any Type subclass). - private final Map, JsonDeserializer> beanDeserializerCache = - new ConcurrentHashMap<>(); - @Override - public Type deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { - ObjectMapper mapper = (ObjectMapper) jp.getCodec(); - JsonNode root = mapper.readTree(jp); + public Type deserialize(JsonParser jp, DeserializationContext ctx) { + JsonNode root = ctx.readTree(jp); JsonNode typeNode = root.get("type"); if (typeNode == null) { return null; @@ -59,29 +47,9 @@ public Type deserialize(JsonParser jp, DeserializationContext ctx) throws IOExce String type = typeNode.asText(); Class typeClass = TYPE_REGISTRY.get(type); if (typeClass == null) { - throw new IOException("unknown type: " + type); + throw DatabindException.from(jp, "unknown type: " + type); } - JsonDeserializer beanDeserializer = beanDeserializerFor(typeClass, ctx); - JsonParser nodeParser = root.traverse(mapper); - nodeParser.nextToken(); - return (Type) beanDeserializer.deserialize(nodeParser, ctx); - } - - private JsonDeserializer beanDeserializerFor( - Class typeClass, DeserializationContext ctx) throws IOException { - JsonDeserializer cached = beanDeserializerCache.get(typeClass); - if (cached != null) { - return cached; - } - JavaType javaType = ctx.constructType(typeClass); - BeanDescription beanDesc = ctx.getConfig().introspect(javaType); - JsonDeserializer beanDeserializer = - ctx.getFactory().createBeanDeserializer(ctx, javaType, beanDesc); - if (beanDeserializer instanceof ResolvableDeserializer) { - ((ResolvableDeserializer) beanDeserializer).resolve(ctx); - } - beanDeserializerCache.put(typeClass, beanDeserializer); - return beanDeserializer; + return ctx.readTreeAsValue(root, typeClass); } } diff --git a/opa-jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/opa-jackson/src/main/resources/META-INF/services/tools.jackson.databind.JacksonModule similarity index 100% rename from opa-jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module rename to opa-jackson/src/main/resources/META-INF/services/tools.jackson.databind.JacksonModule diff --git a/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/MetricsModuleTest.java b/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/MetricsModuleTest.java index 0ba9f42c..4902287d 100644 --- a/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/MetricsModuleTest.java +++ b/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/MetricsModuleTest.java @@ -2,23 +2,22 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.github.open_policy_agent.opa.metrics.Metrics; import io.github.open_policy_agent.opa.metrics.SimpleMetrics; -import java.io.IOException; import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; class MetricsModuleTest { - // Mirrors a real consumer: JavaTimeModule is required for Jackson to handle Duration; the - // MetricsModule shim restores @JsonValue on Metrics.Timer that was lost when SimpleMetrics moved - // into the JSON-free opa-evaluator module. + // The MetricsModule shim restores @JsonValue on Metrics.Timer that was lost when SimpleMetrics + // moved into the JSON-free opa-evaluator module. Jackson 3 has java.time support built-in, + // so no separate module registration is needed for Duration. private final ObjectMapper mapper = - new ObjectMapper().registerModule(new JavaTimeModule()).registerModule(new MetricsModule()); + JsonMapper.builder().addModule(new MetricsModule()).build(); @Test - void timer_jsonShapeMatchesDirectDurationSerialization() throws IOException { + void timer_jsonShapeMatchesDirectDurationSerialization() { SimpleMetrics metrics = new SimpleMetrics(); Metrics.Timer timer = metrics.timer("rego_query_eval"); timer.start(); diff --git a/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReportTest.java b/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReportTest.java index c492c7b5..90c6f984 100644 --- a/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReportTest.java +++ b/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/OpaCoverageReportTest.java @@ -5,8 +5,8 @@ import java.util.List; import java.util.stream.Stream; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/RegoValueModuleTest.java b/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/RegoValueModuleTest.java index 7dc2a520..8532e15e 100644 --- a/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/RegoValueModuleTest.java +++ b/opa-jackson/src/test/java/io/github/open_policy_agent/opa/jackson/RegoValueModuleTest.java @@ -3,7 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import io.github.open_policy_agent.opa.ast.types.RegoArray; import io.github.open_policy_agent.opa.ast.types.RegoBigInt; import io.github.open_policy_agent.opa.ast.types.RegoBoolean; @@ -22,7 +23,7 @@ class RegoValueModuleTest { - private final ObjectMapper mapper = new ObjectMapper().registerModule(new RegoValueModule()); + private final ObjectMapper mapper = JsonMapper.builder().addModule(new RegoValueModule()).build(); // --- Serialization (one test per RegoValue subtype) --- @@ -148,7 +149,7 @@ void deserialize_regoObject_handlesNullValue() throws IOException { @Test void deserialize_regoObject_rejectsNonObjectInput() { assertThatThrownBy(() -> mapper.readValue("[1,2,3]", RegoObject.class)) - .isInstanceOf(IOException.class) + .isInstanceOf(tools.jackson.databind.DatabindException.class) .hasMessageContaining("expected JSON object"); } diff --git a/opa-services/build.gradle.kts b/opa-services/build.gradle.kts index 20d8db5f..0ed9de39 100644 --- a/opa-services/build.gradle.kts +++ b/opa-services/build.gradle.kts @@ -9,10 +9,9 @@ repositories { dependencies { api(project(":opa-evaluator")) - implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.0")) - implementation("com.fasterxml.jackson.core:jackson-databind") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation(platform("tools.jackson:jackson-bom:3.2.0")) + implementation("tools.jackson.core:jackson-databind") + implementation("tools.jackson.dataformat:jackson-dataformat-yaml") implementation("org.apache.commons:commons-compress:1.28.0") // opa-jackson provides the PolicyReader/BundleParser SPI implementations and the // RegoValueModule. Test code in this module bridges JsonNode <-> RegoObject via that module. diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/Opa.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/Opa.java index fc1293de..3ef76e81 100644 --- a/opa-services/src/main/java/io/github/open_policy_agent/opa/Opa.java +++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/Opa.java @@ -1,13 +1,5 @@ package io.github.open_policy_agent.opa; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; -import java.util.*; import io.github.open_policy_agent.opa.ast.types.RegoValue; import io.github.open_policy_agent.opa.bundle.Bundle; import io.github.open_policy_agent.opa.config.Config; @@ -31,6 +23,15 @@ import io.github.open_policy_agent.opa.storage.InMem; import io.github.open_policy_agent.opa.storage.Store; import io.github.open_policy_agent.opa.tracing.Profiler; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.util.*; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.dataformat.yaml.YAMLMapper; /** * Full OPA runtime with plugin management and configuration support. @@ -97,8 +98,8 @@ */ public class Opa { - private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); - private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + private static final ObjectMapper YAML_MAPPER = YAMLMapper.shared(); + private static final ObjectMapper JSON_MAPPER = JsonMapper.shared(); private final String id; private final Logger logger; @@ -208,12 +209,8 @@ public DecisionResult makeDecision(JsonNode input) { } public DecisionResult makeDecision(String input) { - try { - JsonNode inputNode = YAML_MAPPER.readTree(input); - return makeDecision(inputNode); - } catch (IOException e) { - throw new RuntimeException(e); - } + JsonNode inputNode = YAML_MAPPER.readTree(input); + return makeDecision(inputNode); } /** diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java index d6e6031e..7729067b 100644 --- a/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java +++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonDeserialize; import java.security.KeyStore; import java.util.HashMap; import java.util.Map; diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/config/ServicesDeserializer.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/config/ServicesDeserializer.java index dc6cb586..ab915496 100644 --- a/opa-services/src/main/java/io/github/open_policy_agent/opa/config/ServicesDeserializer.java +++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/config/ServicesDeserializer.java @@ -1,12 +1,12 @@ package io.github.open_policy_agent.opa.config; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; /** * Custom deserializer for services configuration. @@ -35,11 +35,11 @@ * *

This deserializer handles both formats and converts them to Map<String, ServiceConfig>. */ -public class ServicesDeserializer extends JsonDeserializer> { +public class ServicesDeserializer extends ValueDeserializer> { @Override public Map deserialize( - JsonParser parser, DeserializationContext ctx) throws IOException { + JsonParser parser, DeserializationContext ctx) { Map services = new LinkedHashMap<>(); @@ -49,9 +49,9 @@ public Map deserialize( // Map format: services: { acmecorp: {...}, other: {...} } parser.nextToken(); // Move to first field name while (parser.currentToken() != JsonToken.END_OBJECT) { - String serviceName = parser.getCurrentName(); + String serviceName = parser.currentName(); parser.nextToken(); // Move to the service config object - Config.ServiceConfig serviceConfig = parser.readValueAs(Config.ServiceConfig.class); + Config.ServiceConfig serviceConfig = ctx.readValue(parser, Config.ServiceConfig.class); serviceConfig.setName(serviceName); services.put(serviceName, serviceConfig); parser.nextToken(); // Move to next field or END_OBJECT @@ -60,17 +60,18 @@ public Map deserialize( // Array format: services: [ {name: acmecorp, ...}, {name: other, ...} ] parser.nextToken(); // Move to first array element while (parser.currentToken() != JsonToken.END_ARRAY) { - Config.ServiceConfig serviceConfig = parser.readValueAs(Config.ServiceConfig.class); + Config.ServiceConfig serviceConfig = ctx.readValue(parser, Config.ServiceConfig.class); String serviceName = serviceConfig.getName(); if (serviceName == null || serviceName.isEmpty()) { - throw new IOException( + throw DatabindException.from( + parser, "Service name is required when using array format for services configuration"); } services.put(serviceName, serviceConfig); parser.nextToken(); // Move to next array element or END_ARRAY } } else { - throw new IOException("Expected object or array for services, got: " + token); + throw DatabindException.from(parser, "Expected object or array for services, got: " + token); } return services; diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DecisionLogPlugin.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DecisionLogPlugin.java index 7384467e..16325e3c 100644 --- a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DecisionLogPlugin.java +++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DecisionLogPlugin.java @@ -1,10 +1,9 @@ package io.github.open_policy_agent.opa.plugins; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.github.open_policy_agent.opa.bundle.Bundle; +import io.github.open_policy_agent.opa.config.Config; +import io.github.open_policy_agent.opa.logging.Logger; +import io.github.open_policy_agent.opa.metrics.Metrics; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.HashSet; @@ -14,10 +13,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import io.github.open_policy_agent.opa.bundle.Bundle; -import io.github.open_policy_agent.opa.config.Config; -import io.github.open_policy_agent.opa.logging.Logger; -import io.github.open_policy_agent.opa.metrics.Metrics; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; /** * Plugin that logs policy decision events. @@ -193,11 +193,8 @@ public void flush() { /** Decision logger that buffers and uploads decision events. */ public static class DecisionLogs { - private static final ObjectMapper MAPPER = new ObjectMapper(); - - static { - MAPPER.registerModule(new JavaTimeModule()); - } + // Jackson 3 has java.time support built-in; no need to register JavaTimeModule. + private static final ObjectMapper MAPPER = JsonMapper.shared(); private final Logger logger; private final PluginManager manager; diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DiscoveryPlugin.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DiscoveryPlugin.java index 6212e30e..1797549b 100644 --- a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DiscoveryPlugin.java +++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/DiscoveryPlugin.java @@ -1,7 +1,6 @@ package io.github.open_policy_agent.opa.plugins; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.open_policy_agent.opa.config.Config; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.HashSet; @@ -12,7 +11,9 @@ import java.util.zip.GZIPInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; -import io.github.open_policy_agent.opa.config.Config; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; /** * Plugin that implements OPA's discovery protocol for dynamic configuration updates. @@ -153,7 +154,7 @@ public void stop() { *

Handles downloading, validation, and activation of discovery configuration. */ private static class DiscoveryBundle extends BundleDownloader { - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper MAPPER = JsonMapper.shared(); private Config discoveredConfig; // Store the last successfully loaded config diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/StatusPlugin.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/StatusPlugin.java index b6184113..98ebf0be 100644 --- a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/StatusPlugin.java +++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/StatusPlugin.java @@ -1,16 +1,17 @@ package io.github.open_policy_agent.opa.plugins; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; +import io.github.open_policy_agent.opa.bundle.Bundle; +import io.github.open_policy_agent.opa.config.Config; +import io.github.open_policy_agent.opa.logging.Logger; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import io.github.open_policy_agent.opa.bundle.Bundle; -import io.github.open_policy_agent.opa.config.Config; -import io.github.open_policy_agent.opa.logging.Logger; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ObjectNode; /** * Plugin that reports OPA runtime status periodically. @@ -101,7 +102,7 @@ public void stop() { /** Status reporter that collects and sends status information. */ public static class Status { - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper MAPPER = JsonMapper.shared(); private final PluginManager manager; private final Logger logger; diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaHotReloadTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaHotReloadTest.java index 74ee9435..3b370a3a 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaHotReloadTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaHotReloadTest.java @@ -11,8 +11,9 @@ import io.github.open_policy_agent.opa.rego.EvaluationContext; import io.github.open_policy_agent.opa.storage.InMem; import io.github.open_policy_agent.opa.storage.Store; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; @@ -32,7 +33,7 @@ */ class OpaHotReloadTest { - private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()); + private static final ObjectMapper MAPPER = JsonMapper.builder().addModule(new io.github.open_policy_agent.opa.jackson.RegoValueModule()).build(); private static final PolicyReader POLICY_READER = ServiceLoader.load(PolicyReader.class).findFirst().orElseThrow(); diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTest.java index 5c3bcd4a..3d6d803b 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTest.java @@ -3,8 +3,8 @@ import static org.junit.jupiter.api.Assertions.*; import io.github.open_policy_agent.opa.ir.PolicyNotFoundException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; import java.io.StringReader; import java.util.Collections; import java.util.Set; diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTwoPhaseInitializationTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTwoPhaseInitializationTest.java index 9f303cd8..1b01a581 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTwoPhaseInitializationTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/OpaTwoPhaseInitializationTest.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ConfigTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ConfigTest.java index 2b5384d0..cad63593 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ConfigTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ConfigTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.dataformat.yaml.YAMLFactory; import java.io.StringReader; import org.junit.jupiter.api.Test; diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ServicesDeserializerTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ServicesDeserializerTest.java index c35ac7f9..75ad46fa 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ServicesDeserializerTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/config/ServicesDeserializerTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.dataformat.yaml.YAMLFactory; import org.junit.jupiter.api.Test; class ServicesDeserializerTest { diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DecisionLogPluginTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DecisionLogPluginTest.java index 1c445cfe..4fd64754 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DecisionLogPluginTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DecisionLogPluginTest.java @@ -3,8 +3,8 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; import java.util.Collections; import java.util.Set; import org.junit.jupiter.api.BeforeEach; diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DiscoveryPluginTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DiscoveryPluginTest.java index a6b40109..cc466604 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DiscoveryPluginTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/DiscoveryPluginTest.java @@ -4,7 +4,7 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/StatusPluginTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/StatusPluginTest.java index ea9bded8..54564e6e 100644 --- a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/StatusPluginTest.java +++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/StatusPluginTest.java @@ -315,8 +315,8 @@ void statusReport_includesInstanceId() throws Exception { java.lang.reflect.Method buildStatusMethod = StatusPlugin.Status.class.getDeclaredMethod("buildStatusReport"); buildStatusMethod.setAccessible(true); - com.fasterxml.jackson.databind.node.ObjectNode report = - (com.fasterxml.jackson.databind.node.ObjectNode) buildStatusMethod.invoke(statusReporter); + tools.jackson.databind.node.ObjectNode report = + (tools.jackson.databind.node.ObjectNode) buildStatusMethod.invoke(statusReporter); // Verify the instance ID is included assertNotNull(report);