From 6ebfe02fad34131800c9a1aa0b27f696a4f77251 Mon Sep 17 00:00:00 2001 From: Gold856 <117957790+Gold856@users.noreply.github.com> Date: Tue, 9 Dec 2025 01:56:49 -0500 Subject: [PATCH 1/3] Fix Jackson being unable to serialize neural network config --- .../NeuralNetworkPropertyManager.java | 2 + .../common/util/file/JacksonUtils.java | 110 +++++++----------- 2 files changed, 44 insertions(+), 68 deletions(-) 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 5f6f05479f..0fa6c31b0d 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 ModelProperties getModel(Path modelPath) { * * @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 40d8e6a4ff..a05b038685 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,10 +17,16 @@ 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.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +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; @@ -38,41 +44,53 @@ public class JacksonUtils { public static class UIMap extends HashMap {} + // 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 new ObjectMapper().readValue(key, Path.class); + } + } + + // 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.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 +99,14 @@ public static T deserialize(String s, Class ref) throws IOException { 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 +114,6 @@ public static T deserialize(Path path, Class ref) throws IOException { 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()) { From 9ba8088b5c26d757010eb7a15fb9f42764a3086c Mon Sep 17 00:00:00 2001 From: Gold856 <117957790+Gold856@users.noreply.github.com> Date: Tue, 9 Dec 2025 02:21:44 -0500 Subject: [PATCH 2/3] Add KeyDeserializer as well --- .../common/util/file/JacksonUtils.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) 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 a05b038685..e2d8e13245 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 @@ -21,22 +21,21 @@ import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 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; @@ -44,6 +43,20 @@ 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 @@ -51,8 +64,7 @@ public Object deserializeKey(String key, DeserializationContext ctxt) throws IOE if (key == null || key.isEmpty()) { return null; } - - return new ObjectMapper().readValue(key, Path.class); + return Paths.get(URI.create(key)); } } @@ -63,6 +75,7 @@ private static ObjectMapper createObjectMapperWithPathSupport(Class baseType) 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()); From 8265763ef817f6622c89fb1495d829aeeca0f8c8 Mon Sep 17 00:00:00 2001 From: Gold856 <117957790+Gold856@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:21:18 -0500 Subject: [PATCH 3/3] Add test --- .../NeuralNetworkPropertyManagerTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 photon-core/src/test/java/org/photonvision/common/configuration/NeuralNetworkPropertyManagerTest.java 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 0000000000..061b4d7d4f --- /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]); + } +}