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

wistefan / tmforum-api / #178

08 Apr 2026 11:06AM UTC coverage: 30.72%. First build
#178

push

GitHub
Merge ff33c34f2 into 56ab62006

409 of 5790 branches covered (7.06%)

Branch coverage included in aggregate %.

0 of 4 new or added lines in 1 file covered. (0.0%)

2769 of 4555 relevant lines covered (60.79%)

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.*;
5
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
6
import com.fasterxml.jackson.databind.util.TokenBuffer;
7
import com.networknt.schema.*;
8
import com.networknt.schema.resource.ClasspathSchemaLoader;
9
import com.networknt.schema.resource.UriSchemaLoader;
10
import lombok.extern.slf4j.Slf4j;
11
import org.fiware.tmforum.common.exception.SchemaValidationException;
12

13
import java.io.IOException;
14
import java.net.URI;
15
import java.util.*;
16
import java.util.stream.Collectors;
17

18
/**
19
 * Deserializer that validates incoming objects against the linked ({@code @schemaLocation}) JSON Schema.
20
 *
21
 * <p>For known sub-types (registered via {@link SubTypePropertyProvider}), sub-type-specific properties
22
 * are recognized and allowed without requiring an explicit {@code @schemaLocation}. Only truly unknown
23
 * properties (those not belonging to any recognized sub-type) are validated against the schema or
24
 * rejected if no schema is provided.</p>
25
 */
26
@Slf4j
×
27
public class ValidatingDeserializer extends DelegatingDeserializer {
28

29
        private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
×
30
        private final BeanDescription beanDescription;
31
        private final List<SubTypePropertyProvider> subTypePropertyProviders;
32

33
        /**
34
         * Create a new ValidatingDeserializer.
35
         *
36
         * @param delegate                  the delegate deserializer
37
         * @param beanDescription           the bean description for the target type
38
         * @param subTypePropertyProviders  the registered sub-type property providers
39
         */
40
        public ValidatingDeserializer(JsonDeserializer<?> delegate, BeanDescription beanDescription,
41
                        List<SubTypePropertyProvider> subTypePropertyProviders) {
42
                super(delegate);
×
43
                this.beanDescription = beanDescription;
×
44
                this.subTypePropertyProviders = subTypePropertyProviders != null
×
45
                                ? subTypePropertyProviders : List.of();
×
46
        }
×
47

48
        @Override
49
        protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
50
                return new ValidatingDeserializer(newDelegatee, beanDescription, subTypePropertyProviders);
×
51
        }
52

53
        @Override
54
        public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
55
                PropertyName schemaLocationProperty = new PropertyName("@schemaLocation");
×
56
                if (beanDescription.findProperties().stream().noneMatch(bpd -> bpd.hasName(schemaLocationProperty))) {
×
57
                        return super.deserialize(p, ctxt);
×
58
                }
59
                // copy the current value into the buffer, so that we can re-read it to validate the schema.
60
                TokenBuffer tokenBuffer = ctxt.bufferAsCopyOfValue(p);
×
61
                Object targetObject = super.deserialize(tokenBuffer.asParserOnFirstToken(), ctxt);
×
62
                if (targetObject instanceof UnknownPreservingBase upb) {
×
63
                        Map<String, Object> unknownProperties = upb.getUnknownProperties();
×
64
                        if (unknownProperties != null && !unknownProperties.isEmpty()) {
×
65
                                // Resolve @type: prefer the dedicated getter, fall back to unknownProperties
66
                                // (some generated UpdateVOs lack an explicit @type field, so it ends up here)
NEW
67
                                String atType = upb.getAtType();
×
NEW
68
                                if (atType == null && unknownProperties.get("@type") instanceof String s) {
×
NEW
69
                                        atType = s;
×
70
                                }
71

72
                                Map<String, Object> trulyUnknown = filterKnownSubTypeProperties(
×
73
                                                unknownProperties, atType);
74
                                // @type is a standard TMForum polymorphism discriminator, not a custom extension
NEW
75
                                trulyUnknown.remove("@type");
×
76

77
                                if (upb.getAtSchemaLocation() != null) {
×
78
                                        // validate only truly unknown properties against the schema
79
                                        if (!trulyUnknown.isEmpty()) {
×
80
                                                String unknownPropsJson = OBJECT_MAPPER.writeValueAsString(trulyUnknown);
×
81
                                                validateWithSchema(upb.getAtSchemaLocation(), unknownPropsJson);
×
82
                                        }
×
83
                                } else if (!trulyUnknown.isEmpty()) {
×
84
                                        throw new SchemaValidationException(List.of(),
×
85
                                                        "If no schema is provided, no additional properties are allowed.");
86
                                }
87
                        }
88
                }
89
                return targetObject;
×
90
        }
91

92
        /**
93
         * Filter out properties that are known to belong to a recognized sub-type.
94
         * Returns only the truly unknown properties that require schema validation.
95
         *
96
         * @param unknownProperties the unknown properties from the parent VO
97
         * @param atType            the {@code @type} value from the payload, may be null
98
         * @return the subset of properties that are not recognized as sub-type fields
99
         */
100
        private Map<String, Object> filterKnownSubTypeProperties(Map<String, Object> unknownProperties,
101
                        String atType) {
102
                if (atType == null || subTypePropertyProviders.isEmpty()) {
×
103
                        return unknownProperties;
×
104
                }
105

106
                Set<String> knownProperties = subTypePropertyProviders.stream()
×
107
                                .map(provider -> provider.getKnownProperties(atType))
×
108
                                .filter(Optional::isPresent)
×
109
                                .map(Optional::get)
×
110
                                .flatMap(Set::stream)
×
111
                                .collect(Collectors.toSet());
×
112

113
                if (knownProperties.isEmpty()) {
×
114
                        return unknownProperties;
×
115
                }
116

117
                Map<String, Object> trulyUnknown = new HashMap<>();
×
118
                unknownProperties.forEach((key, value) -> {
×
119
                        if (!knownProperties.contains(key)) {
×
120
                                trulyUnknown.put(key, value);
×
121
                        }
122
                });
×
123
                return trulyUnknown;
×
124
        }
125

126
        private void validateWithSchema(Object theSchema, String jsonString) {
127
                String schemaAddress = "";
×
128
                if (theSchema instanceof URI schemaUri) {
×
129
                        schemaAddress = schemaUri.toString();
×
130
                } else if (theSchema instanceof String schemaString) {
×
131
                        schemaAddress = schemaString;
×
132
                } else {
133
                        throw new SchemaValidationException(List.of(), "No valid schema address was provided");
×
134
                }
135

136
                try {
137
                        JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(
×
138
                                        SpecVersion.VersionFlag.V202012,
139
                                        builder -> builder.schemaLoaders(sb -> {
×
140
                                                sb.add(new ClasspathSchemaLoader());
×
141
                                                sb.add(new UriSchemaLoader());
×
142
                                        })
×
143
                        );
144
                        SchemaValidatorsConfig.Builder validatorConfigBuilder = SchemaValidatorsConfig.builder();
×
145
                        SchemaValidatorsConfig schemaValidatorsConfig = validatorConfigBuilder.build();
×
146
                        JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(schemaAddress), schemaValidatorsConfig);
×
147
                        Set<ValidationMessage> assertions = schema.validate(jsonString, InputFormat.JSON, executionContext -> {
×
148
                                executionContext.getExecutionConfig().setFormatAssertionsEnabled(true);
×
149
                        });
×
150

151
                        if (!assertions.isEmpty()) {
×
152
                                log.debug("Entity {} is not valid for schema {}. Assertions: {}.", jsonString, schemaAddress, assertions);
×
153
                                throw new SchemaValidationException(assertions.stream().map(ValidationMessage::getMessage).toList(), "Input is not valid for the given schema.");
×
154
                        }
155
                } catch (Exception e) {
×
156
                        if (e instanceof SchemaValidationException) {
×
157
                                throw e;
×
158
                        }
159
                        throw new SchemaValidationException(List.of(), "Was not able to validate the input.", e);
×
160
                }
×
161
        }
×
162
}
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