Skip to content

Commit a0ecb41

Browse files
Gold856spacey-sooty
authored andcommitted
Fix Jackson being unable to deserialize neural network config (PhotonVision#2232)
## Description PhotonVision#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
1 parent e463abc commit a0ecb41

3 files changed

Lines changed: 110 additions & 70 deletions

File tree

photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkPropertyManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.photonvision.common.configuration;
1919

2020
import com.fasterxml.jackson.annotation.JsonCreator;
21+
import com.fasterxml.jackson.annotation.JsonIgnore;
2122
import com.fasterxml.jackson.annotation.JsonProperty;
2223
import java.nio.file.Path;
2324
import java.util.HashMap;
@@ -125,6 +126,7 @@ public ModelProperties getModel(Path modelPath) {
125126
*
126127
* @return A list of all models
127128
*/
129+
@JsonIgnore
128130
public ModelProperties[] getModels() {
129131
return modelPathToProperties.values().toArray(new ModelProperties[0]);
130132
}

photon-core/src/main/java/org/photonvision/common/util/file/JacksonUtils.java

Lines changed: 57 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -17,62 +17,93 @@
1717

1818
package org.photonvision.common.util.file;
1919

20+
import com.fasterxml.jackson.core.JsonGenerator;
2021
import com.fasterxml.jackson.core.json.JsonReadFeature;
22+
import com.fasterxml.jackson.databind.DeserializationContext;
2123
import com.fasterxml.jackson.databind.DeserializationFeature;
2224
import com.fasterxml.jackson.databind.ObjectMapper;
23-
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
25+
import com.fasterxml.jackson.databind.SerializerProvider;
26+
import com.fasterxml.jackson.databind.ext.NioPathDeserializer;
27+
import com.fasterxml.jackson.databind.ext.NioPathSerializer;
2428
import com.fasterxml.jackson.databind.json.JsonMapper;
2529
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
2630
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
2731
import com.fasterxml.jackson.databind.module.SimpleModule;
28-
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
2932
import java.io.File;
3033
import java.io.FileDescriptor;
3134
import java.io.FileOutputStream;
3235
import java.io.IOException;
36+
import java.net.URI;
3337
import java.nio.file.Path;
38+
import java.nio.file.Paths;
3439
import java.util.HashMap;
3540
import java.util.Map;
3641
import org.eclipse.jetty.io.EofException;
3742

3843
public class JacksonUtils {
3944
public static class UIMap extends HashMap<String, Object> {}
4045

46+
// Custom Path key deserializer for Maps with Path keys
47+
public static class PathKeySerializer
48+
extends com.fasterxml.jackson.databind.JsonSerializer<Path> {
49+
@Override
50+
public void serialize(Path value, JsonGenerator gen, SerializerProvider serializers)
51+
throws IOException {
52+
if (value == null) {
53+
gen.writeNull();
54+
} else {
55+
gen.writeFieldName(value.toUri().toString());
56+
}
57+
}
58+
}
59+
60+
// Custom Path key deserializer for Maps with Path keys
61+
public static class PathKeyDeserializer extends com.fasterxml.jackson.databind.KeyDeserializer {
62+
@Override
63+
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
64+
if (key == null || key.isEmpty()) {
65+
return null;
66+
}
67+
return Paths.get(URI.create(key));
68+
}
69+
}
70+
71+
// Helper method to create ObjectMapper with Path serialization support
72+
private static ObjectMapper createObjectMapperWithPathSupport(Class<?> baseType) {
73+
PolymorphicTypeValidator ptv =
74+
BasicPolymorphicTypeValidator.builder().allowIfBaseType(baseType).build();
75+
76+
SimpleModule pathModule = new SimpleModule();
77+
pathModule.addSerializer(Path.class, new NioPathSerializer());
78+
pathModule.addKeySerializer(Path.class, new PathKeySerializer());
79+
pathModule.addDeserializer(Path.class, new NioPathDeserializer());
80+
pathModule.addKeyDeserializer(Path.class, new PathKeyDeserializer());
81+
82+
return JsonMapper.builder()
83+
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
84+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
85+
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
86+
.addModule(pathModule)
87+
.build();
88+
}
89+
4190
public static <T> void serialize(Path path, T object) throws IOException {
4291
serialize(path, object, true);
4392
}
4493

4594
public static <T> String serializeToString(T object) throws IOException {
46-
PolymorphicTypeValidator ptv =
47-
BasicPolymorphicTypeValidator.builder().allowIfBaseType(object.getClass()).build();
48-
ObjectMapper objectMapper =
49-
JsonMapper.builder()
50-
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
51-
.build();
95+
ObjectMapper objectMapper = createObjectMapperWithPathSupport(object.getClass());
5296
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
5397
}
5498

5599
public static <T> void serialize(Path path, T object, boolean forceSync) throws IOException {
56-
PolymorphicTypeValidator ptv =
57-
BasicPolymorphicTypeValidator.builder().allowIfBaseType(object.getClass()).build();
58-
ObjectMapper objectMapper =
59-
JsonMapper.builder()
60-
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
61-
.build();
100+
ObjectMapper objectMapper = createObjectMapperWithPathSupport(object.getClass());
62101
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
63102
saveJsonString(json, path, forceSync);
64103
}
65104

66105
public static <T> T deserialize(Map<?, ?> s, Class<T> ref) throws IOException {
67-
PolymorphicTypeValidator ptv =
68-
BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build();
69-
ObjectMapper objectMapper =
70-
JsonMapper.builder()
71-
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
72-
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
73-
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
74-
.build();
75-
106+
ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref);
76107
return objectMapper.convertValue(s, ref);
77108
}
78109

@@ -81,65 +112,21 @@ public static <T> T deserialize(String s, Class<T> ref) throws IOException {
81112
throw new EofException("Provided empty string for class " + ref.getName());
82113
}
83114

84-
PolymorphicTypeValidator ptv =
85-
BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build();
86-
ObjectMapper objectMapper =
87-
JsonMapper.builder()
88-
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
89-
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
90-
.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
91-
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
92-
.build();
115+
ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref);
116+
objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
93117

94118
return objectMapper.readValue(s, ref);
95119
}
96120

97121
public static <T> T deserialize(Path path, Class<T> ref) throws IOException {
98-
PolymorphicTypeValidator ptv =
99-
BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build();
100-
ObjectMapper objectMapper =
101-
JsonMapper.builder()
102-
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
103-
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
104-
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
105-
.build();
122+
ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref);
106123
File jsonFile = new File(path.toString());
107124
if (jsonFile.exists() && jsonFile.length() > 0) {
108125
return objectMapper.readValue(jsonFile, ref);
109126
}
110127
return null;
111128
}
112129

113-
public static <T> T deserialize(Path path, Class<T> ref, StdDeserializer<T> deserializer)
114-
throws IOException {
115-
ObjectMapper objectMapper = new ObjectMapper();
116-
SimpleModule module = new SimpleModule();
117-
module.addDeserializer(ref, deserializer);
118-
objectMapper.registerModule(module);
119-
120-
File jsonFile = new File(path.toString());
121-
if (jsonFile.exists() && jsonFile.length() > 0) {
122-
return objectMapper.readValue(jsonFile, ref);
123-
}
124-
return null;
125-
}
126-
127-
public static <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer)
128-
throws IOException {
129-
serialize(path, object, ref, serializer, true);
130-
}
131-
132-
public static <T> void serialize(
133-
Path path, T object, Class<T> ref, StdSerializer<T> serializer, boolean forceSync)
134-
throws IOException {
135-
ObjectMapper objectMapper = new ObjectMapper();
136-
SimpleModule module = new SimpleModule();
137-
module.addSerializer(ref, serializer);
138-
objectMapper.registerModule(module);
139-
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
140-
saveJsonString(json, path, forceSync);
141-
}
142-
143130
private static void saveJsonString(String json, Path path, boolean forceSync) throws IOException {
144131
var file = path.toFile();
145132
if (file.getParentFile() != null && !file.getParentFile().exists()) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (C) Photon Vision.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package org.photonvision.common.configuration;
19+
20+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
23+
import java.nio.file.Path;
24+
import java.util.LinkedList;
25+
import org.junit.jupiter.api.Test;
26+
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
27+
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
28+
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
29+
import org.photonvision.common.util.file.JacksonUtils;
30+
31+
public class NeuralNetworkPropertyManagerTest {
32+
@Test
33+
void testSerialization() {
34+
var nnpm = new NeuralNetworkPropertyManager();
35+
// Path is always serialized as absolute; for the test to pass, this must also be made absolute
36+
nnpm.addModelProperties(
37+
new ModelProperties(
38+
Path.of("test", "yolov8nCOCO.rknn").toAbsolutePath(),
39+
"COCO",
40+
new LinkedList<>(),
41+
640,
42+
640,
43+
Family.RKNN,
44+
Version.YOLOV8));
45+
String result = assertDoesNotThrow(() -> JacksonUtils.serializeToString(nnpm));
46+
var deserializedNnpm =
47+
assertDoesNotThrow(
48+
() -> JacksonUtils.deserialize(result, NeuralNetworkPropertyManager.class));
49+
assertEquals(nnpm.getModels()[0], deserializedNnpm.getModels()[0]);
50+
}
51+
}

0 commit comments

Comments
 (0)