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

torand / openapi2java / 18202480060

02 Oct 2025 06:44PM UTC coverage: 81.561% (-0.8%) from 82.388%
18202480060

push

github

web-flow
Merge pull request #48 from torand/refactor-info-classes

Refactor info classes

507 of 736 branches covered (68.89%)

Branch coverage included in aggregate %.

825 of 934 new or added lines in 38 files covered. (88.33%)

11 existing lines in 7 files now uncovered.

1541 of 1775 relevant lines covered (86.82%)

5.09 hits per line

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

85.07
/src/main/java/io/github/torand/openapi2java/generators/ModelGenerator.java
1
/*
2
 * Copyright (c) 2024-2025 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.openapi2java.generators;
17

18
import io.github.torand.openapi2java.collectors.*;
19
import io.github.torand.openapi2java.model.EnumInfo;
20
import io.github.torand.openapi2java.model.PojoInfo;
21
import io.github.torand.openapi2java.model.TypeInfo;
22
import io.github.torand.openapi2java.utils.OpenApi2JavaException;
23
import io.github.torand.openapi2java.writers.EnumWriter;
24
import io.github.torand.openapi2java.writers.PojoWriter;
25
import io.swagger.v3.oas.models.OpenAPI;
26
import io.swagger.v3.oas.models.Operation;
27
import io.swagger.v3.oas.models.media.Schema;
28
import io.swagger.v3.oas.models.parameters.Parameter;
29
import io.swagger.v3.oas.models.responses.ApiResponse;
30
import org.slf4j.Logger;
31
import org.slf4j.LoggerFactory;
32

33
import java.io.IOException;
34
import java.util.Collection;
35
import java.util.HashSet;
36
import java.util.Optional;
37
import java.util.Set;
38
import java.util.concurrent.atomic.AtomicInteger;
39
import java.util.stream.Stream;
40

41
import static io.github.torand.javacommons.collection.CollectionHelper.isEmpty;
42
import static io.github.torand.javacommons.collection.CollectionHelper.nonEmpty;
43
import static io.github.torand.javacommons.lang.Exceptions.illegalStateException;
44
import static io.github.torand.javacommons.lang.StringHelper.nonBlank;
45
import static io.github.torand.javacommons.lang.StringHelper.stripTail;
46
import static io.github.torand.javacommons.stream.StreamHelper.streamSafely;
47
import static io.github.torand.openapi2java.collectors.SchemaResolver.isObjectType;
48
import static io.github.torand.openapi2java.utils.StringUtils.pluralSuffix;
49
import static io.github.torand.openapi2java.writers.WriterFactory.createEnumWriter;
50
import static io.github.torand.openapi2java.writers.WriterFactory.createPojoWriter;
51
import static java.util.Objects.nonNull;
52
import static java.util.stream.Collectors.toSet;
53

54
/**
55
 * Generates source code for models (pojos).
56
 */
57
public class ModelGenerator {
58
    private static final Logger logger = LoggerFactory.getLogger(ModelGenerator.class);
4✔
59

60
    private final Options opts;
61

62
    public ModelGenerator(Options opts) {
2✔
63
        this.opts = opts;
3✔
64
    }
1✔
65

66
    public void generate(OpenAPI openApiDoc) {
67
        ComponentResolver componentResolver = new ComponentResolver(openApiDoc);
5✔
68

69
        // Generate pojos and enums referenced by included tags only
70
        Set<String> relevantPojos = getRelevantPojos(openApiDoc, componentResolver);
5✔
71

72
        AtomicInteger enumCount = new AtomicInteger(0);
5✔
73
        AtomicInteger pojoCount = new AtomicInteger(0);
5✔
74

75
        openApiDoc.getComponents().getSchemas().forEach((name, schema) -> {
10✔
76
            String pojoName = name + opts.pojoNameSuffix();
6✔
77
            if (relevantPojos.contains(pojoName)) {
4✔
78

79
                if (isEnum(schema)) {
4✔
80
                    generateEnumFile(pojoName, schema);
4✔
81
                    enumCount.incrementAndGet();
3✔
82
                }
83

84
                if (isClass(schema)) {
4✔
85
                    generatePojoFile(pojoName, schema, componentResolver.schemas());
6✔
86
                    pojoCount.incrementAndGet();
3✔
87
                }
88
            }
89
        });
1✔
90

91
        if (logger.isInfoEnabled()) {
3!
92
            logger.info("Generated {} enum{}, {} pojo{} in directory {}", enumCount.get(), pluralSuffix(enumCount.get()), pojoCount.get(), pluralSuffix(pojoCount.get()), opts.getModelOutputDir(null));
36✔
93
        }
94
    }
1✔
95

96
    private void generateEnumFile(String name, Schema<?> schema) {
97
        if (opts.verbose()) {
4!
98
            logger.info("Generating model enum {}", name);
4✔
99
        }
100

101
        EnumInfoCollector enumInfoCollector = new EnumInfoCollector(opts);
6✔
102
        EnumInfo enumInfo = enumInfoCollector.getEnumInfo(name, schema);
5✔
103

104
        String enumFilename = name + opts.getFileExtension();
6✔
105
        try (EnumWriter enumWriter = createEnumWriter(enumFilename, opts, enumInfo.modelSubdir())) {
7✔
106
            enumWriter.write(enumInfo);
3✔
107
        } catch (IOException e) {
×
NEW
108
            throw new OpenApi2JavaException("Failed to write file %s".formatted(enumFilename), e);
×
109
        }
1✔
110
    }
1✔
111

112
    private void generatePojoFile(String name, Schema<?> schema, SchemaResolver schemaResolver) {
113
        if (opts.verbose()) {
4!
114
            logger.info("Generating model class {}", name);
4✔
115
        }
116

117
        PojoInfoCollector pojoInfoCollector = new PojoInfoCollector(schemaResolver, opts);
7✔
118
        PojoInfo pojoInfo = pojoInfoCollector.getPojoInfo(name, schema);
5✔
119

120
        String pojoFilename = name + opts.getFileExtension();
6✔
121
        try (PojoWriter pojoWriter = createPojoWriter(pojoFilename, opts, pojoInfo.modelSubdir())) {
7✔
122
            pojoWriter.write(pojoInfo);
3✔
123
        } catch (IOException e) {
×
NEW
124
            throw new OpenApi2JavaException("Failed to write file %s".formatted(pojoFilename), e);
×
125
        }
1✔
126
    }
1✔
127

128
    private Set<String> getRelevantPojos(OpenAPI openApiDoc, ComponentResolver componentResolver) {
129
        record PathOperation(String path, String method, Operation operation) {}
12✔
130

131
        return openApiDoc.getPaths().entrySet().stream()
6✔
132
            .flatMap(entry -> {
4✔
133
                var path = entry.getKey();
4✔
134
                var pathItem = entry.getValue();
4✔
135

136
                return Stream.of(
12✔
137
                    new PathOperation(path, "GET", pathItem.getGet()),
10✔
138
                    new PathOperation(path, "PUT", pathItem.getPut()),
10✔
139
                    new PathOperation(path, "POST", pathItem.getPost()),
10✔
140
                    new PathOperation(path, "DELETE", pathItem.getDelete()),
10✔
141
                    new PathOperation(path, "PATCH", pathItem.getPatch())
3✔
142
                ).filter(po -> po.operation != null);
8✔
143
            })
144
            .map(pathOperation -> {
2✔
145
                try {
146
                    if (opts.verbose()) {
4!
147
                        logger.info("Getting relevant Pojos for {} {}", pathOperation.method, pathOperation.path);
7✔
148
                    }
149

150
                    return getRelevantPojosForOperation(pathOperation.operation(), componentResolver);
6✔
151
                } catch (RuntimeException e) {
×
152
                    throw new IllegalArgumentException("Failed to get relevant Pojos for %s %s"
×
153
                        .formatted(pathOperation.method, pathOperation.path), e);
×
154
                }
155
            })
156
            .flatMap(Collection::stream)
1✔
157
            .collect(toSet());
3✔
158
    }
159

160
    private Set<String> getRelevantPojosForOperation(Operation operation, ComponentResolver componentResolver) {
161
        TypeInfoCollector typeInfoCollector = new TypeInfoCollector(componentResolver.schemas(), opts);
8✔
162

163
        Set<String> relevantPojos = new HashSet<>();
4✔
164

165
        if (isEmpty(operation.getTags()) || isRelevantTag(operation)) {
8!
166
            if (nonEmpty(operation.getParameters())) {
4✔
167
                operation.getParameters().forEach(parameter -> {
8✔
168
                    Parameter realParameter = parameter;
2✔
169
                    if (nonBlank(parameter.get$ref())) {
4!
170
                        realParameter = componentResolver.parameters().getOrThrow(parameter.get$ref());
6✔
171
                    }
172

173
                    if (nonNull(realParameter.getSchema())) {
4!
174
                        getPojoTypeName(realParameter.getSchema(), typeInfoCollector).ifPresent(relevantPojos::add);
11✔
175
                    }
176

177
                    if (nonEmpty(realParameter.getContent())) {
4!
178
                        realParameter.getContent().forEach((contentType, mediaType) -> {
×
179
                            if (nonNull(mediaType.getSchema())) {
×
180
                                getPojoTypeName(mediaType.getSchema(), typeInfoCollector).ifPresent(relevantPojos::add);
×
181
                            }
182
                        });
×
183
                    }
184
                });
1✔
185
            }
186
            if (nonNull(operation.getRequestBody()) && nonEmpty(operation.getRequestBody().getContent())) {
9!
187
                operation.getRequestBody().getContent().forEach((contentType, mediaType) -> {
8✔
188
                    if (nonNull(mediaType.getSchema())) {
4!
189
                        getPojoTypeName(mediaType.getSchema(), typeInfoCollector).ifPresent(relevantPojos::add);
11✔
190
                    }
191
                });
1✔
192
            }
193
            if (nonEmpty(operation.getResponses())) {
4!
194
                operation.getResponses().forEach((code, response) -> {
8✔
195
                    ApiResponse realResponse = response;
2✔
196
                    if (nonBlank(response.get$ref())) {
4✔
197
                        realResponse = componentResolver.responses().getOrThrow(response.get$ref());
6✔
198
                    }
199

200
                    if (nonEmpty(realResponse.getContent())) {
4✔
201
                        realResponse.getContent().forEach((contentType, mediaType) -> {
7✔
202

203
                            if (nonNull(mediaType.getSchema())) {
4!
204
                                getPojoTypeName(mediaType.getSchema(), typeInfoCollector).ifPresent(relevantPojos::add);
11✔
205
                            }
206
                        });
1✔
207
                    }
208
                });
1✔
209
            }
210
        }
211

212
        relevantPojos.addAll(getNestedPojos(relevantPojos, componentResolver.schemas()));
8✔
213

214
        return relevantPojos;
2✔
215
    }
216

217
    private Set<String> getNestedPojos(Set<String> parentPojos, SchemaResolver schemaResolver) {
218
        TypeInfoCollector typeInfoCollector = new TypeInfoCollector(schemaResolver, opts);
7✔
219

220
        Set<String> nestedPojos = new HashSet<>();
4✔
221
        parentPojos.forEach(pojo -> {
7✔
222
            String schemaRef = "#/components/schemas/" + stripTail(pojo, opts.pojoNameSuffix().length());
8✔
223
            schemaResolver.get(schemaRef).ifPresent(schema -> nestedPojos.addAll(getNestedSchemaTypes(schema, schemaResolver, typeInfoCollector)));
18✔
224
        });
1✔
225

226
        return nestedPojos;
2✔
227
    }
228

229
    private Set<String> getNestedSchemaTypes(Schema<?> parentSchema, SchemaResolver schemaResolver, TypeInfoCollector typeInfoCollector) {
230
        Set<String> schemaTypes = new HashSet<>();
4✔
231

232
        if (nonEmpty(parentSchema.getAllOf())) {
4✔
233
            parentSchema.getAllOf().forEach(subSchema -> schemaTypes.addAll(getNestedSchemaTypes(subSchema, schemaResolver, typeInfoCollector)));
18✔
234
        } else if (nonEmpty(parentSchema.getOneOf())) {
4✔
235
            Schema<?> subSchema = typeInfoCollector.getNonNullableSubSchema(parentSchema.getOneOf())
7✔
236
                .orElseThrow(illegalStateException("Schema 'oneOf' must contain a non-nullable sub-schema"));
4✔
237
            schemaTypes.addAll(getNestedSchemaTypes(subSchema, schemaResolver, typeInfoCollector));
8✔
238
        } else if (nonBlank(parentSchema.get$ref())) {
5✔
239
            getPojoTypeName(parentSchema, typeInfoCollector).ifPresent(schemaTypes::add);
10✔
240
            Schema<?> $refSchema = schemaResolver.getOrThrow(parentSchema.get$ref());
5✔
241
            schemaTypes.addAll(getNestedSchemaTypes($refSchema, schemaResolver, typeInfoCollector));
8✔
242
        } else if (nonEmpty(parentSchema.getProperties())) {
5✔
243
            parentSchema.getProperties().forEach((propName, propSchema) ->
9✔
244
                schemaTypes.addAll(getNestedSchemaTypes(propSchema, schemaResolver, typeInfoCollector))
9✔
245
            );
246
        } else if (nonNull(parentSchema.getItems())) {
4✔
247
            schemaTypes.addAll(getNestedSchemaTypes(parentSchema.getItems(), schemaResolver, typeInfoCollector));
9✔
248
        }
249

250
        return schemaTypes;
2✔
251
    }
252

253
    private Optional<String> getPojoTypeName(Schema<?> schema, TypeInfoCollector typeInfoCollector) {
254
        if (isObjectType(schema)) {
3✔
255
            return Optional.empty(); // Inline type, not a component
2✔
256
        }
257
        TypeInfo bodyType = typeInfoCollector.getTypeInfo(schema);
4✔
258
        if (nonNull(bodyType.itemType())) {
4✔
259
            return Optional.of(bodyType.itemType().name());
5✔
260
        } else {
261
            return Optional.of(bodyType.name());
4✔
262
        }
263
    }
264

265
    private boolean isRelevantTag(Operation operation) {
266
        return isEmpty(opts.includeTags()) || streamSafely(operation.getTags()).anyMatch(tag -> opts.includeTags().contains(tag));
8!
267
    }
268

269
    private boolean isEnum(Schema<?> schema) {
270
        return streamSafely(schema.getTypes()).anyMatch("string"::equals) && nonNull(schema.getEnum());
15!
271
    }
272

273
    private boolean isClass(Schema<?> schema) {
274
        return streamSafely(schema.getTypes()).anyMatch("object"::equals) || nonNull(schema.getAllOf());
15✔
275
    }
276
}
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