• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wistefan / tmforum-api / #191

05 Jun 2026 12:39PM UTC coverage: 31.277%. First build
#191

push

web-flow
Merge 38ccbfe0d into e3fa6674f

436 of 5832 branches covered (7.48%)

Branch coverage included in aggregate %.

39 of 46 new or added lines in 2 files covered. (84.78%)

2834 of 4623 relevant lines covered (61.3%)

0.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/common/src/main/java/org/fiware/tmforum/common/mapping/ValidatingDeserializer.java
1
package org.fiware.tmforum.common.mapping;
2

3
import com.fasterxml.jackson.core.JsonParser;
4
import com.fasterxml.jackson.databind.BeanDescription;
5
import com.fasterxml.jackson.databind.DeserializationContext;
6
import com.fasterxml.jackson.databind.JsonDeserializer;
7
import com.fasterxml.jackson.databind.ObjectMapper;
8
import com.fasterxml.jackson.databind.PropertyName;
9
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
10
import com.fasterxml.jackson.databind.util.TokenBuffer;
11
import com.networknt.schema.InputFormat;
12
import com.networknt.schema.JsonSchema;
13
import com.networknt.schema.JsonSchemaFactory;
14
import com.networknt.schema.SchemaLocation;
15
import com.networknt.schema.SchemaValidatorsConfig;
16
import com.networknt.schema.SpecVersion;
17
import com.networknt.schema.ValidationMessage;
18
import com.networknt.schema.resource.ClasspathSchemaLoader;
19
import com.networknt.schema.resource.UriSchemaLoader;
20
import lombok.extern.slf4j.Slf4j;
21
import org.fiware.tmforum.common.exception.SchemaValidationException;
22

23
import java.io.IOException;
24
import java.net.URI;
25
import java.util.HashMap;
26
import java.util.List;
27
import java.util.Map;
28
import java.util.Optional;
29
import java.util.Set;
30
import java.util.stream.Collectors;
31

32
/**
33
 * Deserializer that validates incoming objects against the linked ({@code @schemaLocation}) JSON Schema.
34
 *
35
 * <p>For known sub-types (registered via {@link SubTypePropertyProvider}), sub-type-specific properties
36
 * are recognized and allowed without requiring an explicit {@code @schemaLocation}. Only truly unknown
37
 * properties (those not belonging to any recognized sub-type) are validated against the schema or
38
 * rejected if no schema is provided.</p>
39
 */
40
@Slf4j
×
41
public class ValidatingDeserializer extends DelegatingDeserializer {
42

43
        private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
×
44
        private final BeanDescription beanDescription;
45
        private final List<SubTypePropertyProvider> subTypePropertyProviders;
46

47
        /**
48
         * Create a new ValidatingDeserializer.
49
         *
50
         * @param delegate                  the delegate deserializer
51
         * @param beanDescription           the bean description for the target type
52
         * @param subTypePropertyProviders  the registered sub-type property providers
53
         */
54
        public ValidatingDeserializer(JsonDeserializer<?> delegate, BeanDescription beanDescription,
55
                        List<SubTypePropertyProvider> subTypePropertyProviders) {
56
                super(delegate);
×
57
                this.beanDescription = beanDescription;
×
58
                this.subTypePropertyProviders = subTypePropertyProviders != null
×
59
                                ? subTypePropertyProviders : List.of();
×
60
        }
×
61

62
        @Override
63
        protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
64
                return new ValidatingDeserializer(newDelegatee, beanDescription, subTypePropertyProviders);
×
65
        }
66

67
        @Override
68
        public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
69
                PropertyName schemaLocationProperty = new PropertyName("@schemaLocation");
×
70
                if (beanDescription.findProperties().stream().noneMatch(bpd -> bpd.hasName(schemaLocationProperty))) {
×
71
                        return super.deserialize(p, ctxt);
×
72
                }
73
                // copy the current value into the buffer, so that we can re-read it to validate the schema.
74
                TokenBuffer tokenBuffer = ctxt.bufferAsCopyOfValue(p);
×
75
                Object targetObject = super.deserialize(tokenBuffer.asParserOnFirstToken(), ctxt);
×
76
                if (targetObject instanceof UnknownPreservingBase upb) {
×
77
                        Map<String, Object> unknownProperties = upb.getUnknownProperties();
×
78
                        if (unknownProperties != null && !unknownProperties.isEmpty()) {
×
79
                                // Resolve @type: prefer the dedicated getter, fall back to unknownProperties
80
                                // (some generated UpdateVOs lack an explicit @type field, so it ends up here)
81
                                String atType = upb.getAtType();
×
82
                                if (atType == null && unknownProperties.get("@type") instanceof String s) {
×
83
                                        atType = s;
×
84
                                }
85

86
                                Map<String, Object> trulyUnknown = filterKnownSubTypeProperties(
×
87
                                                unknownProperties, atType);
88
                                // @type is a standard TMForum polymorphism discriminator, not a custom extension
89
                                trulyUnknown.remove("@type");
×
90

91
                                if (upb.getAtSchemaLocation() != null) {
×
92
                                        // validate only truly unknown properties against the schema
93
                                        if (!trulyUnknown.isEmpty()) {
×
94
                                                String unknownPropsJson = OBJECT_MAPPER.writeValueAsString(trulyUnknown);
×
95
                                                validateWithSchema(upb.getAtSchemaLocation(), unknownPropsJson);
×
96
                                        }
×
97
                                } else if (!trulyUnknown.isEmpty()) {
×
NEW
98
                                        throw new SchemaValidationException(
×
NEW
99
                                                        List.of("Additional properties not allowed: " + trulyUnknown.keySet()),
×
100
                                                        "No additional properties are allowed without a @schemaLocation.");
101
                                }
102
                        }
103
                }
104
                return targetObject;
×
105
        }
106

107
        /**
108
         * Filter out properties that are known to belong to a recognized sub-type.
109
         * Returns only the truly unknown properties that require schema validation.
110
         *
111
         * @param unknownProperties the unknown properties from the parent VO
112
         * @param atType            the {@code @type} value from the payload, may be null
113
         * @return the subset of properties that are not recognized as sub-type fields
114
         */
115
        private Map<String, Object> filterKnownSubTypeProperties(Map<String, Object> unknownProperties,
116
                        String atType) {
117
                if (atType == null || subTypePropertyProviders.isEmpty()) {
×
118
                        return unknownProperties;
×
119
                }
120

121
                Set<String> knownProperties = subTypePropertyProviders.stream()
×
122
                                .map(provider -> provider.getKnownProperties(atType))
×
123
                                .filter(Optional::isPresent)
×
124
                                .map(Optional::get)
×
125
                                .flatMap(Set::stream)
×
126
                                .collect(Collectors.toSet());
×
127

128
                if (knownProperties.isEmpty()) {
×
129
                        return unknownProperties;
×
130
                }
131

132
                Map<String, Object> trulyUnknown = new HashMap<>();
×
133
                unknownProperties.forEach((key, value) -> {
×
134
                        if (!knownProperties.contains(key)) {
×
135
                                trulyUnknown.put(key, value);
×
136
                        }
137
                });
×
138
                return trulyUnknown;
×
139
        }
140

141
        private void validateWithSchema(Object theSchema, String jsonString) {
142
                String schemaAddress = "";
×
143
                if (theSchema instanceof URI schemaUri) {
×
144
                        schemaAddress = schemaUri.toString();
×
145
                } else if (theSchema instanceof String schemaString) {
×
146
                        schemaAddress = schemaString;
×
147
                } else {
148
                        throw new SchemaValidationException(List.of(), "No valid schema address was provided");
×
149
                }
150

151
                try {
152
                        JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(
×
153
                                        SpecVersion.VersionFlag.V202012,
154
                                        builder -> builder.schemaLoaders(sb -> {
×
155
                                                sb.add(new ClasspathSchemaLoader());
×
156
                                                sb.add(new UriSchemaLoader());
×
157
                                        })
×
158
                        );
159
                        SchemaValidatorsConfig.Builder validatorConfigBuilder = SchemaValidatorsConfig.builder();
×
160
                        SchemaValidatorsConfig schemaValidatorsConfig = validatorConfigBuilder.build();
×
161
                        JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(schemaAddress), schemaValidatorsConfig);
×
162
                        Set<ValidationMessage> assertions = schema.validate(jsonString, InputFormat.JSON, executionContext -> {
×
163
                                executionContext.getExecutionConfig().setFormatAssertionsEnabled(true);
×
164
                        });
×
165

166
                        if (!assertions.isEmpty()) {
×
167
                                log.debug("Entity {} is not valid for schema {}. Assertions: {}.", jsonString, schemaAddress, assertions);
×
168
                                throw new SchemaValidationException(assertions.stream().map(ValidationMessage::getMessage).toList(), "Input is not valid for the given schema.");
×
169
                        }
170
                } catch (Exception e) {
×
171
                        if (e instanceof SchemaValidationException) {
×
172
                                throw e;
×
173
                        }
174
                        throw new SchemaValidationException(List.of(), "Was not able to validate the input.", e);
×
175
                }
×
176
        }
×
177
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc