mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-25 01:41:40 +00:00
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
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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<String, Object> {}
|
||||
|
||||
// Custom Path key deserializer for Maps with Path keys
|
||||
public static class PathKeySerializer
|
||||
extends com.fasterxml.jackson.databind.JsonSerializer<Path> {
|
||||
@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 <T> void serialize(Path path, T object) throws IOException {
|
||||
serialize(path, object, true);
|
||||
}
|
||||
|
||||
public static <T> 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 <T> 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> T deserialize(Map<?, ?> s, Class<T> 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> T deserialize(Path path, Class<T> 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> T deserialize(Path path, Class<T> ref, StdDeserializer<T> 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 <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer)
|
||||
throws IOException {
|
||||
serialize(path, object, ref, serializer, true);
|
||||
}
|
||||
|
||||
public static <T> void serialize(
|
||||
Path path, T object, Class<T> ref, StdSerializer<T> 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()) {
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user