From c2433e03325f070c1f78711730eda201e77656a3 Mon Sep 17 00:00:00 2001 From: Gold856 <117957790+Gold856@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:09:49 -0500 Subject: [PATCH] Fix Jackson being unable to deserialize neural network config (#2232) ## Description #2224 removed the custom deserializers for `Path`, but we still need one to be able to deserialize the `Path` key in `NeuralNetworkPropertyManager`. Additionally, Jackson seems to auto-convert the `Path` key to a `String` using `toString` instead of its own serializers, so a custom key serializer is also needed to consistently use the same format for paths. This also removes unused serde methods in `JacksonUtils` to minimize potential future churn, and tacks an `@JsonIgnore` on `getModels` to prevent Jackson from serializing a `ModelProperty` array into the database. ## Meta Merge checklist: - [x] Pull Request title is [short, imperative summary](https://cbea.ms/git-commit/) of proposed changes - [x] The description documents the _what_ and _why_ - [ ] If this PR changes behavior or adds a feature, user documentation is updated - [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly - [ ] If this PR touches configuration, this is backwards compatible with settings back to v2025.3.2 - [ ] If this PR touches pipeline settings or anything related to data exchange, the frontend typing is updated - [x] If this PR addresses a bug, a regression test for it is added --- .../NeuralNetworkPropertyManager.java | 2 + .../common/util/file/JacksonUtils.java | 127 ++++++++---------- .../NeuralNetworkPropertyManagerTest.java | 51 +++++++ 3 files changed, 110 insertions(+), 70 deletions(-) create mode 100644 photon-core/src/test/java/org/photonvision/common/configuration/NeuralNetworkPropertyManagerTest.java diff --git a/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkPropertyManager.java b/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkPropertyManager.java index 5f6f05479..0fa6c31b0 100644 --- a/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkPropertyManager.java +++ b/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkPropertyManager.java @@ -18,6 +18,7 @@ package org.photonvision.common.configuration; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import java.nio.file.Path; import java.util.HashMap; @@ -125,6 +126,7 @@ public class NeuralNetworkPropertyManager { * * @return A list of all models */ + @JsonIgnore public ModelProperties[] getModels() { return modelPathToProperties.values().toArray(new ModelProperties[0]); } diff --git a/photon-core/src/main/java/org/photonvision/common/util/file/JacksonUtils.java b/photon-core/src/main/java/org/photonvision/common/util/file/JacksonUtils.java index 40d8e6a4f..e2d8e1324 100644 --- a/photon-core/src/main/java/org/photonvision/common/util/file/JacksonUtils.java +++ b/photon-core/src/main/java/org/photonvision/common/util/file/JacksonUtils.java @@ -17,20 +17,25 @@ package org.photonvision.common.util.file; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ext.NioPathDeserializer; +import com.fasterxml.jackson.databind.ext.NioPathSerializer; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; +import java.net.URI; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import org.eclipse.jetty.io.EofException; @@ -38,41 +43,67 @@ import org.eclipse.jetty.io.EofException; public class JacksonUtils { public static class UIMap extends HashMap {} + // Custom Path key deserializer for Maps with Path keys + public static class PathKeySerializer + extends com.fasterxml.jackson.databind.JsonSerializer { + @Override + public void serialize(Path value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeFieldName(value.toUri().toString()); + } + } + } + + // Custom Path key deserializer for Maps with Path keys + public static class PathKeyDeserializer extends com.fasterxml.jackson.databind.KeyDeserializer { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException { + if (key == null || key.isEmpty()) { + return null; + } + return Paths.get(URI.create(key)); + } + } + + // Helper method to create ObjectMapper with Path serialization support + private static ObjectMapper createObjectMapperWithPathSupport(Class baseType) { + PolymorphicTypeValidator ptv = + BasicPolymorphicTypeValidator.builder().allowIfBaseType(baseType).build(); + + SimpleModule pathModule = new SimpleModule(); + pathModule.addSerializer(Path.class, new NioPathSerializer()); + pathModule.addKeySerializer(Path.class, new PathKeySerializer()); + pathModule.addDeserializer(Path.class, new NioPathDeserializer()); + pathModule.addKeyDeserializer(Path.class, new PathKeyDeserializer()); + + return JsonMapper.builder() + .configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT) + .addModule(pathModule) + .build(); + } + public static void serialize(Path path, T object) throws IOException { serialize(path, object, true); } public static String serializeToString(T object) throws IOException { - PolymorphicTypeValidator ptv = - BasicPolymorphicTypeValidator.builder().allowIfBaseType(object.getClass()).build(); - ObjectMapper objectMapper = - JsonMapper.builder() - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT) - .build(); + ObjectMapper objectMapper = createObjectMapperWithPathSupport(object.getClass()); return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); } public static void serialize(Path path, T object, boolean forceSync) throws IOException { - PolymorphicTypeValidator ptv = - BasicPolymorphicTypeValidator.builder().allowIfBaseType(object.getClass()).build(); - ObjectMapper objectMapper = - JsonMapper.builder() - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT) - .build(); + ObjectMapper objectMapper = createObjectMapperWithPathSupport(object.getClass()); String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); saveJsonString(json, path, forceSync); } public static T deserialize(Map s, Class ref) throws IOException { - PolymorphicTypeValidator ptv = - BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build(); - ObjectMapper objectMapper = - JsonMapper.builder() - .configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT) - .build(); - + ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref); return objectMapper.convertValue(s, ref); } @@ -81,28 +112,14 @@ public class JacksonUtils { throw new EofException("Provided empty string for class " + ref.getName()); } - PolymorphicTypeValidator ptv = - BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build(); - ObjectMapper objectMapper = - JsonMapper.builder() - .configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT) - .build(); + ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref); + objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); return objectMapper.readValue(s, ref); } public static T deserialize(Path path, Class ref) throws IOException { - PolymorphicTypeValidator ptv = - BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build(); - ObjectMapper objectMapper = - JsonMapper.builder() - .configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT) - .build(); + ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref); File jsonFile = new File(path.toString()); if (jsonFile.exists() && jsonFile.length() > 0) { return objectMapper.readValue(jsonFile, ref); @@ -110,36 +127,6 @@ public class JacksonUtils { return null; } - public static T deserialize(Path path, Class ref, StdDeserializer deserializer) - throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(ref, deserializer); - objectMapper.registerModule(module); - - File jsonFile = new File(path.toString()); - if (jsonFile.exists() && jsonFile.length() > 0) { - return objectMapper.readValue(jsonFile, ref); - } - return null; - } - - public static void serialize(Path path, T object, Class ref, StdSerializer serializer) - throws IOException { - serialize(path, object, ref, serializer, true); - } - - public static void serialize( - Path path, T object, Class ref, StdSerializer serializer, boolean forceSync) - throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(ref, serializer); - objectMapper.registerModule(module); - String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); - saveJsonString(json, path, forceSync); - } - private static void saveJsonString(String json, Path path, boolean forceSync) throws IOException { var file = path.toFile(); if (file.getParentFile() != null && !file.getParentFile().exists()) { diff --git a/photon-core/src/test/java/org/photonvision/common/configuration/NeuralNetworkPropertyManagerTest.java b/photon-core/src/test/java/org/photonvision/common/configuration/NeuralNetworkPropertyManagerTest.java new file mode 100644 index 000000000..061b4d7d4 --- /dev/null +++ b/photon-core/src/test/java/org/photonvision/common/configuration/NeuralNetworkPropertyManagerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.common.configuration; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Path; +import java.util.LinkedList; +import org.junit.jupiter.api.Test; +import org.photonvision.common.configuration.NeuralNetworkModelManager.Family; +import org.photonvision.common.configuration.NeuralNetworkModelManager.Version; +import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties; +import org.photonvision.common.util.file.JacksonUtils; + +public class NeuralNetworkPropertyManagerTest { + @Test + void testSerialization() { + var nnpm = new NeuralNetworkPropertyManager(); + // Path is always serialized as absolute; for the test to pass, this must also be made absolute + nnpm.addModelProperties( + new ModelProperties( + Path.of("test", "yolov8nCOCO.rknn").toAbsolutePath(), + "COCO", + new LinkedList<>(), + 640, + 640, + Family.RKNN, + Version.YOLOV8)); + String result = assertDoesNotThrow(() -> JacksonUtils.serializeToString(nnpm)); + var deserializedNnpm = + assertDoesNotThrow( + () -> JacksonUtils.deserialize(result, NeuralNetworkPropertyManager.class)); + assertEquals(nnpm.getModels()[0], deserializedNnpm.getModels()[0]); + } +}