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

torand / jsonschema2java / 22553968753

01 Mar 2026 10:11PM UTC coverage: 83.689% (+1.3%) from 82.424%
22553968753

push

github

torand
feat: add config parameter 'durationClassName' to specify a custom class to represent string schemas with format "duration" in generated code

328 of 433 branches covered (75.75%)

Branch coverage included in aggregate %.

16 of 16 new or added lines in 5 files covered. (100.0%)

41 existing lines in 6 files now uncovered.

888 of 1020 relevant lines covered (87.06%)

5.25 hits per line

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

88.91
/src/main/java/io/github/torand/jsonschema2java/collectors/TypeInfoCollector.java
1
/*
2
 * Copyright (c) 2024-2026 Tore Eide Andersen
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package io.github.torand.jsonschema2java.collectors;
17

18
import io.github.torand.jsonschema2java.generators.Options;
19
import io.github.torand.jsonschema2java.model.AnnotationInfo;
20
import io.github.torand.jsonschema2java.model.TypeInfo;
21
import io.github.torand.jsonschema2java.utils.JsonSchemaDef;
22

23
import java.net.URI;
24
import java.util.ArrayList;
25
import java.util.List;
26
import java.util.Optional;
27

28
import static io.github.torand.javacommons.collection.CollectionHelper.isEmpty;
29
import static io.github.torand.javacommons.lang.Exceptions.illegalStateException;
30
import static io.github.torand.javacommons.lang.StringHelper.nonBlank;
31
import static io.github.torand.jsonschema2java.collectors.Extensions.EXT_JSON_DESERIALIZER;
32
import static io.github.torand.jsonschema2java.collectors.Extensions.EXT_JSON_FORMAT;
33
import static io.github.torand.jsonschema2java.collectors.Extensions.EXT_JSON_SERIALIZER;
34
import static io.github.torand.jsonschema2java.collectors.Extensions.EXT_NULLABLE;
35
import static io.github.torand.jsonschema2java.collectors.Extensions.EXT_VALIDATION_CONSTRAINT;
36
import static io.github.torand.jsonschema2java.collectors.TypeInfoCollector.NullabilityResolution.FORCE_NOT_NULLABLE;
37
import static io.github.torand.jsonschema2java.collectors.TypeInfoCollector.NullabilityResolution.FORCE_NULLABLE;
38
import static io.github.torand.jsonschema2java.utils.PackageUtils.getClassNameFromFqn;
39
import static io.github.torand.jsonschema2java.utils.StringUtils.joinCsv;
40
import static java.lang.Boolean.TRUE;
41
import static java.util.Objects.nonNull;
42
import static java.util.function.Predicate.not;
43

44
/**
45
 * Collects information about a type from a schema.
46
 */
47
public class TypeInfoCollector extends BaseCollector {
48
    public enum NullabilityResolution {FROM_SCHEMA, FORCE_NULLABLE, FORCE_NOT_NULLABLE}
21✔
49

50
    private final SchemaResolver schemaResolver;
51

52
    public TypeInfoCollector(Options opts, SchemaResolver schemaResolver) {
53
        super(opts);
3✔
54
        this.schemaResolver = schemaResolver;
3✔
55
    }
1✔
56

57
    public TypeInfo getTypeInfo(JsonSchemaDef schema) {
58
        return getTypeInfo(schema, NullabilityResolution.FROM_SCHEMA);
5✔
59
    }
60

61
    public TypeInfo getTypeInfo(JsonSchemaDef schema, NullabilityResolution nullabilityResolution) {
62
        if (!schema.hasTypes()) {
3✔
63

64
            boolean nullable = isNullable(schema, nullabilityResolution);
5✔
65

66
            if (schema.hasAnyOf()) {
3!
UNCOV
67
                throw new IllegalStateException("Schema 'anyOf' not supported");
×
68
            }
69

70
            if (schema.hasOneOf()) {
3✔
71
                // Limited support for 'oneOf' in properties: use the first non-nullable subschema
72
                JsonSchemaDef subSchema = getNonNullableSubSchema(schema.oneOf().toList())
8✔
73
                    .orElseThrow(illegalStateException("Schema 'oneOf' must contain a non-nullable sub-schema"));
4✔
74

75
                return getTypeInfo(subSchema, nullable ? FORCE_NULLABLE : FORCE_NOT_NULLABLE);
8!
76
            }
77

78
            if (schema.allOf().count() == 1) {
6!
79
                // 'allOf' only supported if it contains single type
UNCOV
80
                JsonSchemaDef firstSchema = schema.allOf().findFirst().orElseThrow();
×
UNCOV
81
                return getTypeInfo(firstSchema, nullable ? FORCE_NULLABLE : FORCE_NOT_NULLABLE);
×
82
            }
83

84
            URI ref = schema.ref();
3✔
85
            if (nonNull(ref)) {
3!
86
                TypeInfo typeInfo;
87

88
                if (schemaResolver.isPrimitiveType(ref)  || schemaResolver.isArrayType(ref)) {
10!
89
                    JsonSchemaDef refSchema = schemaResolver.getOrThrow(ref);
5✔
90
                    typeInfo = getTypeInfo(refSchema, nullable ? FORCE_NULLABLE : FORCE_NOT_NULLABLE);
9✔
91
                } else {
1✔
92
                    typeInfo = new TypeInfo()
4✔
93
                        .withName(SchemaResolver.getTypeName(ref) + opts.pojoNameSuffix())
7✔
94
                        .withNullable(nullable);
2✔
95

96
                    String modelSubpackage = schemaResolver.getModelSubpackage(ref).orElse(null);
8✔
97
                    typeInfo = typeInfo.withAddedNormalImport(opts.getModelPackage(modelSubpackage) + "." + typeInfo.name());
10✔
98

99
                    if (!schemaResolver.isEnumType(schema.ref())) {
6✔
100
                        AnnotationInfo validAnnotation = getValidAnnotation();
3✔
101
                        typeInfo = typeInfo.withAddedAnnotation(validAnnotation);
4✔
102
                    }
103
                    if (!nullable) {
2✔
104
                        AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
105
                        typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
106
                    }
107
                }
108

109
                if (nonBlank(schema.description())) {
4✔
110
                    typeInfo = typeInfo.withDescription(schema.description());
5✔
111
                }
112

113
                return typeInfo;
2✔
UNCOV
114
            } else if (schema.hasAllOf()) {
×
UNCOV
115
                throw new IllegalStateException("No types, no $ref: %s".formatted(schema.toString()));
×
116
            }
117
        }
118

119
        return getJsonType(schema, nullabilityResolution);
5✔
120
    }
121

122
    public Optional<JsonSchemaDef> getNonNullableSubSchema(List<JsonSchemaDef> subSchemas) {
123
        return subSchemas.stream()
5✔
124
            .filter(not(this::isNullable))
2✔
125
            .findFirst();
1✔
126
    }
127

128
    private TypeInfo getJsonType(JsonSchemaDef schema, NullabilityResolution nullabilityResolution) {
129
        TypeInfo typeInfo = new TypeInfo()
4✔
130
            .withDescription(schema.description())
3✔
131
            .withPrimitive(true)
4✔
132
            .withNullable(isNullable(schema, nullabilityResolution));
3✔
133

134
        String jsonType = schema.types()
4✔
135
            .filter(not("null"::equals))
2✔
136
            .findFirst()
7✔
137
            .orElseThrow(illegalStateException("Unexpected types: %s", schema.toString()));
6✔
138

139
        if ("string".equals(jsonType)) {
4✔
140
            typeInfo = populateJsonStringType(typeInfo, schema);
6✔
141
        } else if ("number".equals(jsonType)) {
4✔
142
            typeInfo = populateJsonNumberType(typeInfo, schema);
6✔
143
        } else if ("integer".equals(jsonType)) {
4✔
144
            typeInfo = populateJsonIntegerType(typeInfo, schema);
6✔
145
        } else if ("boolean".equals(jsonType)) {
4✔
146
            typeInfo = populateJsonBooleanType(typeInfo);
5✔
147
        } else if ("array".equals(jsonType)) {
4✔
148
            typeInfo = populateJsonArrayType(typeInfo, schema);
6✔
149
        } else if ("object".equals(jsonType) && isEmpty(schema.properties()) && schema.additionalProperties() instanceof JsonSchemaDef) {
12!
150
            typeInfo = populateJsonMapType(typeInfo, schema);
6✔
151
        } else {
152
            // Schema not expected to be defined "inline" using type 'object'
UNCOV
153
            throw new IllegalStateException("Unexpected schema: %s".formatted(schema.toString()));
×
154
        }
155

156
        Optional<String> maybeJsonSerializer = schema.extensions().getString(EXT_JSON_SERIALIZER);
5✔
157
        if (maybeJsonSerializer.isPresent()) {
3✔
158
            AnnotationInfo jsonSerializeAnnotation = getJsonSerializeAnnotation(maybeJsonSerializer.get());
6✔
159
            typeInfo = typeInfo.withAddedAnnotation(jsonSerializeAnnotation);
4✔
160
        }
161

162
        Optional<String> maybeJsonDeserializer = schema.extensions().getString(EXT_JSON_DESERIALIZER);
5✔
163
        if (maybeJsonDeserializer.isPresent()) {
3✔
164
            AnnotationInfo jsonDeserializeAnnotation = getJsonDeserializeAnnotation(maybeJsonDeserializer.get());
6✔
165
            typeInfo = typeInfo.withAddedAnnotation(jsonDeserializeAnnotation);
4✔
166
        }
167

168
        Optional<String> maybeValidationConstraint = schema.extensions().getString(EXT_VALIDATION_CONSTRAINT);
5✔
169
        if (maybeValidationConstraint.isPresent()) {
3✔
170
            AnnotationInfo validationConstraintAnnotation = new AnnotationInfo(
8✔
171
                "@%s".formatted(getClassNameFromFqn(maybeValidationConstraint.get())),
6✔
172
                maybeValidationConstraint.get()
4✔
173
            );
174
            typeInfo = typeInfo.withAddedAnnotation(validationConstraintAnnotation);
4✔
175
        }
176

177
        return typeInfo;
2✔
178
    }
179

180
    private TypeInfo populateJsonStringType(TypeInfo typeInfo, JsonSchemaDef schema) {
181
        if ("uri".equals(schema.format())) {
5✔
182
            typeInfo = typeInfo.withName("URI")
4✔
183
                .withSchemaFormat(schema.format())
3✔
184
                .withAddedNormalImport("java.net.URI");
2✔
185
            if (!typeInfo.nullable() && opts.addJakartaBeanValidationAnnotations()) {
7!
186
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
187
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
188
            }
1✔
189
        } else if ("uuid".equals(schema.format())) {
5✔
190
            typeInfo = typeInfo.withName("UUID")
4✔
191
                .withSchemaFormat(schema.format())
3✔
192
                .withAddedNormalImport("java.util.UUID");
2✔
193
            if (!typeInfo.nullable() && opts.addJakartaBeanValidationAnnotations()) {
7!
194
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
195
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
196
            }
1✔
197
        } else if ("duration".equals(schema.format())) {
5✔
198
            typeInfo = typeInfo.withName(getClassNameFromFqn(opts.durationClassName()))
7✔
199
                .withSchemaFormat(schema.format())
4✔
200
                .withAddedNormalImport(opts.durationClassName());
3✔
201
            if (!typeInfo.nullable() && opts.addJakartaBeanValidationAnnotations()) {
7!
202
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
203
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
204
            }
1✔
205
        } else if ("date".equals(schema.format())) {
5✔
206
            typeInfo = typeInfo.withName(getClassNameFromFqn(opts.dateClassName()))
7✔
207
                .withSchemaFormat(schema.format())
4✔
208
                .withAddedNormalImport(opts.dateClassName());
3✔
209
            if (!typeInfo.nullable() && opts.addJakartaBeanValidationAnnotations()) {
7!
210
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
211
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
212
            }
213
            Optional<String> maybeJsonFormat = schema.extensions().getString(EXT_JSON_FORMAT);
5✔
214
            if (maybeJsonFormat.isPresent()) {
3✔
215
                AnnotationInfo jsonFormatAnnotation = getJsonFormatAnnotation(maybeJsonFormat.get());
6✔
216
                typeInfo = typeInfo.withAddedAnnotation(jsonFormatAnnotation);
4✔
217
            }
218
        } else if ("date-time".equals(schema.format())) {
6✔
219
            typeInfo = typeInfo.withName(getClassNameFromFqn(opts.dateTimeClassName()))
7✔
220
                .withSchemaFormat(schema.format())
4✔
221
                .withAddedNormalImport(opts.dateTimeClassName());
3✔
222
            if (!typeInfo.nullable() && opts.addJakartaBeanValidationAnnotations()) {
7!
223
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
224
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
225
            }
226
            Optional<String> maybeJsonFormat = schema.extensions().getString(EXT_JSON_FORMAT);
5✔
227
            if (maybeJsonFormat.isPresent()) {
3✔
228
                AnnotationInfo jsonFormatAnnotation = getJsonFormatAnnotation(maybeJsonFormat.get());
6✔
229
                typeInfo = typeInfo.withAddedAnnotation(jsonFormatAnnotation);
4✔
230
            }
231
        } else if ("email".equals(schema.format())) {
6✔
232
            typeInfo = typeInfo.withName("String")
4✔
233
                .withSchemaFormat(schema.format());
3✔
234
            if (opts.addJakartaBeanValidationAnnotations()) {
4!
235
                if (!typeInfo.nullable()) {
3✔
236
                    AnnotationInfo notBlankAnnotation = getNotBlankAnnotation();
3✔
237
                    typeInfo = typeInfo.withAddedAnnotation(notBlankAnnotation);
4✔
238
                }
239
                AnnotationInfo emailAnnotation = getEmailAnnotation();
3✔
240
                typeInfo = typeInfo.withAddedAnnotation(emailAnnotation);
4✔
241
            }
1✔
242
        } else if ("binary".equals(schema.format())) {
5✔
243
            typeInfo = typeInfo.withName("byte[]")
4✔
244
                .withSchemaFormat(schema.format());
3✔
245
            if (opts.addJakartaBeanValidationAnnotations()) {
4!
246
                if (!typeInfo.nullable()) {
3!
247
                    AnnotationInfo notEmptyAnnotation = getNotEmptyAnnotation();
3✔
248
                    typeInfo = typeInfo.withAddedAnnotation(notEmptyAnnotation);
4✔
249
                }
250
                if (nonNull(schema.minItems()) || nonNull(schema.maxItems())) {
8!
UNCOV
251
                    AnnotationInfo sizeAnnotaion = getArraySizeAnnotation(schema);
×
UNCOV
252
                    typeInfo = typeInfo.withAddedAnnotation(sizeAnnotaion);
×
UNCOV
253
                }
×
254
            }
255
        } else {
256
            typeInfo = typeInfo.withName("String")
4✔
257
                .withSchemaFormat(schema.format());
3✔
258
            if (opts.addJakartaBeanValidationAnnotations()) {
4!
259
                if (!typeInfo.nullable()) {
3✔
260
                    AnnotationInfo notBlankAnnotation = getNotBlankAnnotation();
3✔
261
                    typeInfo = typeInfo.withAddedAnnotation(notBlankAnnotation);
4✔
262
                }
263
                if (nonBlank(schema.pattern())) {
4✔
264
                    typeInfo = typeInfo.withSchemaPattern(schema.pattern())
6✔
265
                        .withAddedAnnotation(getPatternAnnotation(schema));
3✔
266
                }
267
                if (nonNull(schema.minLength()) || nonNull(schema.maxLength())) {
8✔
268
                    typeInfo = typeInfo
2✔
269
                        .withSchemaMinLength(schema.minLength())
3✔
270
                        .withSchemaMaxLength(schema.maxLength());
3✔
271
                    AnnotationInfo sizeAnnotation = getStringSizeAnnotation(schema);
4✔
272
                    typeInfo = typeInfo.withAddedAnnotation(sizeAnnotation);
4✔
273
                }
274
            }
275
        }
276

277
        return typeInfo;
2✔
278
    }
279

280
    private TypeInfo populateJsonNumberType(TypeInfo typeInfo, JsonSchemaDef schema) {
281
        if ("double".equals(schema.format())) {
5✔
282
            typeInfo = typeInfo.withName("Double");
5✔
283
        } else if ("float".equals(schema.format())) {
5✔
284
            typeInfo = typeInfo.withName("Float");
5✔
285
        } else {
286
            typeInfo = typeInfo.withName("BigDecimal")
4✔
287
                .withAddedNormalImport("java.math.BigDecimal");
2✔
288
        }
289
        typeInfo = typeInfo.withSchemaFormat(schema.format());
5✔
290
        if (opts.addJakartaBeanValidationAnnotations()) {
4!
291
            if (!typeInfo.nullable()) {
3✔
292
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
293
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
294
            }
295
            if ("BigDecimal".equals(typeInfo.name())) {
5✔
296
                if (nonNull(schema.minimum())) {
4✔
297
                    AnnotationInfo minAnnotation = getMinAnnotation(schema);
4✔
298
                    typeInfo = typeInfo.withAddedAnnotation(minAnnotation);
4✔
299
                }
300
                if (nonNull(schema.maximum())) {
4✔
301
                    AnnotationInfo maxAnnotation = getMaxAnnotation(schema);
4✔
302
                    typeInfo = typeInfo.withAddedAnnotation(maxAnnotation);
4✔
303
                }
304
            }
305
        }
306

307
        return typeInfo;
2✔
308
    }
309

310
    private TypeInfo populateJsonIntegerType(TypeInfo typeInfo, JsonSchemaDef schema) {
311
        typeInfo = typeInfo.withName("int64".equals(schema.format()) ? "Long" :"Integer")
11✔
312
            .withSchemaFormat(schema.format());
3✔
313

314
        if (opts.addJakartaBeanValidationAnnotations()) {
4!
315
            if (!typeInfo.nullable()) {
3✔
316
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
317
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
318
            }
319
            if (nonNull(schema.minimum())) {
4✔
320
                AnnotationInfo minAnnotation = getMinAnnotation(schema);
4✔
321
                typeInfo = typeInfo.withAddedAnnotation(minAnnotation);
4✔
322
            }
323
            if (nonNull(schema.maximum())) {
4✔
324
                AnnotationInfo maxAnnotation = getMaxAnnotation(schema);
4✔
325
                typeInfo = typeInfo.withAddedAnnotation(maxAnnotation);
4✔
326
            }
327
        }
328

329
        return typeInfo;
2✔
330
    }
331

332
    private TypeInfo populateJsonBooleanType(TypeInfo typeInfo) {
333
        typeInfo = typeInfo.withName("Boolean");
4✔
334
        if (!typeInfo.nullable() && opts.addJakartaBeanValidationAnnotations()) {
7!
335
            AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
336
            typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
337
        }
338

339
        return typeInfo;
2✔
340
    }
341

342
    private TypeInfo populateJsonArrayType(TypeInfo typeInfo, JsonSchemaDef schema) {
343
        typeInfo = typeInfo.withPrimitive(false);
4✔
344
        if (TRUE.equals(schema.uniqueItems())) {
6✔
345
            typeInfo = typeInfo.withName("Set")
4✔
346
                .withAddedNormalImport("java.util.Set");
3✔
347
        } else {
348
            typeInfo = typeInfo.withName("List")
4✔
349
                .withAddedNormalImport("java.util.List");
2✔
350
        }
351

352
        if (opts.addJakartaBeanValidationAnnotations()) {
4!
353
            AnnotationInfo validAnnotation = getValidAnnotation();
3✔
354
            typeInfo = typeInfo.withAddedAnnotation(validAnnotation);
4✔
355
        }
356

357
        TypeInfo itemType = getTypeInfo(schema.items());
5✔
358

359
        if (opts.addJakartaBeanValidationAnnotations()) {
4!
360
            if (!typeInfo.nullable()) {
3✔
361
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
362
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
363
            }
364
            if (nonNull(schema.minItems()) || nonNull(schema.maxItems())) {
8✔
365
                AnnotationInfo sizeAnnotation = getArraySizeAnnotation(schema);
4✔
366
                typeInfo = typeInfo.withAddedAnnotation(sizeAnnotation);
4✔
367
            }
368
        }
369

370
        return typeInfo.withItemType(itemType);
4✔
371
    }
372

373
    private TypeInfo populateJsonMapType(TypeInfo typeInfo, JsonSchemaDef schema) {
374
        typeInfo = typeInfo.withName("Map")
4✔
375
            .withAddedNormalImport("java.util.Map");
2✔
376

377
        if (opts.addJakartaBeanValidationAnnotations()) {
4!
378
            AnnotationInfo validAnnotation = getValidAnnotation();
3✔
379
            typeInfo = typeInfo.withAddedAnnotation(validAnnotation);
4✔
380
        }
381

382
        TypeInfo keyTypeInfo = new TypeInfo().withName("String");
6✔
383
        if (opts.addJakartaBeanValidationAnnotations()) {
4!
384
            AnnotationInfo notBlankAnnotation = getNotBlankAnnotation();
3✔
385
            keyTypeInfo = keyTypeInfo.withAddedAnnotation(notBlankAnnotation);
4✔
386
        }
387

388
        typeInfo = typeInfo.withKeyType(keyTypeInfo)
5✔
389
            .withItemType(getTypeInfo((JsonSchemaDef)schema.additionalProperties()));
5✔
390

391
        if (opts.addJakartaBeanValidationAnnotations()) {
4!
392
            if (!typeInfo.nullable()) {
3!
393
                AnnotationInfo notNullAnnotation = getNotNullAnnotation();
3✔
394
                typeInfo = typeInfo.withAddedAnnotation(notNullAnnotation);
4✔
395
            }
396
            if (nonNull(schema.minItems()) || nonNull(schema.maxItems())) {
4!
397
                AnnotationInfo sizeAnnotation = getArraySizeAnnotation(schema);
4✔
398
                typeInfo = typeInfo.withAddedAnnotation(sizeAnnotation);
4✔
399
            }
400
        }
401

402
        return typeInfo;
2✔
403
    }
404

405
    private boolean isNullable(JsonSchemaDef schema, NullabilityResolution resolution) {
406
        return switch(resolution) {
4✔
407
            case FROM_SCHEMA -> isNullable(schema);
4✔
408
            case FORCE_NULLABLE -> true;
2✔
409
            case FORCE_NOT_NULLABLE -> false;
1✔
410
        };
411
    }
412

413
    public boolean isNullable(JsonSchemaDef schema) {
414
        if (!schema.hasTypes()) {
3✔
415
            if (schema.hasAllOf()) {
3!
UNCOV
416
                return schema.allOf().allMatch(this::isNullable);
×
417
            } else if (schema.hasOneOf()) {
3✔
418
                return schema.oneOf().anyMatch(this::isNullable);
6✔
419
            } else if (nonNull(schema.ref())) {
4!
420
                return isNullableByExtension(schema);
4✔
421
            } else {
UNCOV
422
                throw new IllegalStateException("No types, no $ref: %s".formatted(schema.toString()));
×
423
            }
424
        }
425

426
        return schema.hasType("null") || isNullableByExtension(schema);
12✔
427
    }
428

429
    private boolean isNullableByExtension(JsonSchemaDef schema) {
430
        return schema.extensions().getBoolean(EXT_NULLABLE).orElse(false);
10✔
431
    }
432

433
    private AnnotationInfo getJsonSerializeAnnotation(String jsonSerializer) {
434
        return new AnnotationInfo("@JsonSerialize(using = %s)".formatted(getClassRefFromFullyQualifiedClassName(jsonSerializer)))
15✔
435
            .withAddedNormalImport("com.fasterxml.jackson.databind.annotation.JsonSerialize")
2✔
436
            .withAddedNormalImport(jsonSerializer);
1✔
437
    }
438

439
    private AnnotationInfo getJsonDeserializeAnnotation(String jsonDeserializer) {
440
        return new AnnotationInfo("@JsonDeserialize(using = %s)".formatted(getClassRefFromFullyQualifiedClassName(jsonDeserializer)))
15✔
441
            .withAddedNormalImport("com.fasterxml.jackson.databind.annotation.JsonDeserialize")
2✔
442
            .withAddedNormalImport(jsonDeserializer);
1✔
443
    }
444

445
    private AnnotationInfo getJsonFormatAnnotation(String pattern) {
446
        return new AnnotationInfo(
10✔
447
            "@JsonFormat(pattern = \"%s\")".formatted(pattern),
3✔
448
            "com.fasterxml.jackson.annotation.JsonFormat");
449
    }
450

451
    private AnnotationInfo getValidAnnotation() {
452
        return new AnnotationInfo(
6✔
453
            "@Valid",
454
            "jakarta.validation.Valid");
455
    }
456

457
    private AnnotationInfo getNotNullAnnotation() {
458
        return new AnnotationInfo(
6✔
459
            "@NotNull",
460
            "jakarta.validation.constraints.NotNull");
461
    }
462

463
    private AnnotationInfo getNotBlankAnnotation() {
464
        return new AnnotationInfo(
6✔
465
            "@NotBlank",
466
            "jakarta.validation.constraints.NotBlank");
467
    }
468

469
    private AnnotationInfo getNotEmptyAnnotation() {
470
        return new AnnotationInfo(
6✔
471
            "@NotEmpty",
472
            "jakarta.validation.constraints.NotEmpty");
473
    }
474

475
    private AnnotationInfo getMinAnnotation(JsonSchemaDef schema) {
476
        return new AnnotationInfo(
9✔
477
            "@Min(%d)".formatted(schema.minimum().longValue()),
7✔
478
            "jakarta.validation.constraints.Min");
479
    }
480

481
    private AnnotationInfo getMaxAnnotation(JsonSchemaDef schema) {
482
        return new AnnotationInfo(
9✔
483
            "@Max(%d)".formatted(schema.maximum().longValue()),
7✔
484
            "jakarta.validation.constraints.Max");
485
    }
486

487
    private AnnotationInfo getPatternAnnotation(JsonSchemaDef schema) {
488
        return new AnnotationInfo(
9✔
489
            "@Pattern(regexp = \"%s\")".formatted(schema.pattern()),
5✔
490
            "jakarta.validation.constraints.Pattern");
491
    }
492

493
    private AnnotationInfo getEmailAnnotation() {
494
        return new AnnotationInfo(
6✔
495
            "@Email",
496
            "jakarta.validation.constraints.Email");
497
    }
498

499
    private AnnotationInfo getArraySizeAnnotation(JsonSchemaDef schema) {
500
        List<String> sizeParams = new ArrayList<>();
4✔
501
        if (nonNull(schema.minItems())) {
4✔
502
            sizeParams.add("min = %d".formatted(schema.minItems()));
12✔
503
        }
504
        if (nonNull(schema.maxItems())) {
4✔
505
            sizeParams.add("max = %d".formatted(schema.maxItems()));
12✔
506
        }
507
        return new AnnotationInfo(
9✔
508
            "@Size(%s)".formatted(joinCsv(sizeParams)),
5✔
509
            "jakarta.validation.constraints.Size");
510
    }
511

512
    private AnnotationInfo getStringSizeAnnotation(JsonSchemaDef schema) {
513
        List<String> sizeParams = new ArrayList<>();
4✔
514
        if (nonNull(schema.minLength())) {
4✔
515
            sizeParams.add("min = %d".formatted(schema.minLength()));
12✔
516
        }
517
        if (nonNull(schema.maxLength())) {
4✔
518
            sizeParams.add("max = %d".formatted(schema.maxLength()));
12✔
519
        }
520
        return new AnnotationInfo(
9✔
521
            "@Size(%s)".formatted(joinCsv(sizeParams)),
5✔
522
            "jakarta.validation.constraints.Size");
523
    }
524

525
    private String getClassRefFromFullyQualifiedClassName(String fullyQualifiedClassName) {
526
        String className = getClassNameFromFqn(fullyQualifiedClassName);
3✔
527
        return formatClassRef(className);
4✔
528
    }
529
}
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