Skip to content

Commit 6d27f68

Browse files
committed
feat: add support for array types
1 parent 254b605 commit 6d27f68

File tree

7 files changed

+201
-39
lines changed

7 files changed

+201
-39
lines changed

src/main/java/es/nachobrito/jsonschema/compiler/domain/JavaName.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@
1616

1717
package es.nachobrito.jsonschema.compiler.domain;
1818

19+
import java.util.Objects;
1920
import java.util.regex.Pattern;
2021

2122
public class JavaName {
2223
private static final Pattern jsonIdPattern = Pattern.compile("[\\W_]([a-z])");
2324

24-
public static String fromJsonIdentifier(String identifier) {
25-
var formatted = jsonIdPattern.matcher(identifier).replaceAll(m -> m.group(1).toUpperCase());
26-
return formatted;
25+
public static String variableFromJsonIdentifier(String identifier) {
26+
Objects.requireNonNull(identifier);
27+
return jsonIdPattern.matcher(identifier).replaceAll(m -> m.group(1).toUpperCase());
28+
}
29+
30+
public static String classFromJsonIdentifier(String identifier) {
31+
Objects.requireNonNull(identifier);
32+
var variableName = variableFromJsonIdentifier(identifier);
33+
return variableName.substring(0, 1).toUpperCase() + variableName.substring(1);
2734
}
2835
}

src/main/java/es/nachobrito/jsonschema/compiler/domain/Property.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@
1717
package es.nachobrito.jsonschema.compiler.domain;
1818

1919
import java.lang.constant.ClassDesc;
20-
import java.util.regex.Pattern;
2120

2221
public record Property(String key, ClassDesc type, String formattedName) {
2322

2423
public Property(String key, ClassDesc type){
25-
this(key, type, JavaName.fromJsonIdentifier(key));
24+
this(key, type, JavaName.variableFromJsonIdentifier(key));
2625
}
2726

2827
}

src/main/java/es/nachobrito/jsonschema/compiler/domain/generator/HashCodeGenerator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ private void loadPrimitiveValue(String propertyName, ClassDesc propertyDesc, Cod
9595
}
9696

9797
private void loadArrayValue(String propertyName, ClassDesc propertyDesc, CodeBuilder cob) {
98-
9998
cob.aload(0)
10099
.getfield(classDesc, propertyName, propertyDesc)
101100
.invokestatic(
102101
ClassDesc.of(Arrays.class.getName()),
103102
"deepHashcode",
104-
MethodTypeDesc.of(CD_Object.arrayType()));
103+
MethodTypeDesc.of(CD_int, CD_Object.arrayType()))
104+
;
105105
}
106106
}

src/main/java/es/nachobrito/jsonschema/compiler/domain/schemareader/AbstractSchemaReader.java

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package es.nachobrito.jsonschema.compiler.domain.schemareader;
1818

19-
import static java.lang.constant.ClassDesc.of;
2019
import static java.lang.constant.ConstantDescs.*;
2120
import static java.util.stream.Collectors.toMap;
2221

@@ -26,17 +25,14 @@
2625
import es.nachobrito.jsonschema.compiler.domain.Schema;
2726
import java.io.IOException;
2827
import java.lang.constant.ClassDesc;
29-
import java.net.Inet4Address;
30-
import java.net.Inet6Address;
3128
import java.net.URI;
3229
import java.nio.file.Files;
3330
import java.nio.file.Path;
34-
import java.time.*;
3531
import java.util.*;
3632

3733
public abstract class AbstractSchemaReader implements SchemaReader {
3834
private Map<String, Object> models;
39-
private Map<String, Schema> schemas = new HashMap<>();
35+
private final Map<String, Schema> schemas = new HashMap<>();
4036

4137
@Override
4238
public List<Schema> read(URI uri) {
@@ -86,12 +82,42 @@ private ClassDesc getJavaType(
8682
var jsonSchemaType = (String) property.get("type");
8783
var jsonSchemaFormat = (String) property.get("format");
8884
return switch (jsonSchemaType) {
89-
case "string" -> getJavaStringType(jsonSchemaFormat);
9085
case "integer" -> CD_Integer;
9186
case "number" -> CD_Double;
9287
case "boolean" -> CD_Boolean;
93-
// case "array" -> ?;
88+
case "array" -> getArrayType(propertyKey);
9489
case "object" -> getJavaObjectType(propertyKey);
90+
case "string" -> StringFormat.toClassDesc(jsonSchemaFormat);
91+
default -> CD_Object;
92+
};
93+
}
94+
95+
private ClassDesc getArrayType(String propertyKey) {
96+
var definition = getModelPropertyDefinition(propertyKey, getRootProperties());
97+
@SuppressWarnings("unchecked")
98+
var items = (Map<String, ?>) definition.get("items");
99+
100+
// todo: there is no support for Tuples in Java, so arrays with prefixItems will be treated as
101+
// Object[] for now, until a better solution is found.
102+
// see: https://json-schema.org/understanding-json-schema/reference/array#tupleValidation
103+
if (items == null) {
104+
return CD_Object.arrayType();
105+
}
106+
107+
var itemsTypeDefinition = (String) items.get("type");
108+
var itemsTypeFormat = (String) items.get("format");
109+
var properties = (Map<String, Map<String, ?>>) items.get("properties");
110+
111+
return switch (itemsTypeDefinition) {
112+
case "integer" -> CD_Integer.arrayType();
113+
case "number" -> CD_Double.arrayType();
114+
case "boolean" -> CD_Boolean.arrayType();
115+
// case "array" -> getArrayType(propertyKey);
116+
case "object" ->
117+
getJavaObjectType(
118+
JavaName.classFromJsonIdentifier("%s_item".formatted(propertyKey)), properties)
119+
.arrayType();
120+
case "string" -> StringFormat.toClassDesc(itemsTypeFormat).arrayType();
95121
default -> CD_Object;
96122
};
97123
}
@@ -100,10 +126,11 @@ private ClassDesc getJavaObjectType(String propertyKey) {
100126
var definition = getModelPropertyDefinition(propertyKey, getRootProperties());
101127
var properties = definition.get("properties");
102128
var name = getPropertyName(propertyKey, definition);
103-
var schema =
104-
schemas.computeIfAbsent(
105-
name,
106-
it -> new Schema(it, processProperties((Map<String, Map<String, ?>>) properties)));
129+
return getJavaObjectType(name, (Map<String, Map<String, ?>>) properties);
130+
}
131+
132+
private ClassDesc getJavaObjectType(String name, Map<String, Map<String, ?>> properties) {
133+
var schema = schemas.computeIfAbsent(name, it -> new Schema(it, processProperties(properties)));
107134
return ClassDesc.of(schema.className());
108135
}
109136

@@ -112,7 +139,7 @@ private String getPropertyName(String propertyKey, Map<String, ?> definition) {
112139
return (String) definition.get("title");
113140
}
114141

115-
return JavaName.fromJsonIdentifier("%s_%s".formatted(getRootClassName(), propertyKey));
142+
return JavaName.variableFromJsonIdentifier("%s_%s".formatted(getRootClassName(), propertyKey));
116143
}
117144

118145
private Map<String, ?> getModelPropertyDefinition(
@@ -130,26 +157,6 @@ private String getPropertyName(String propertyKey, Map<String, ?> definition) {
130157
models.getOrDefault("properties", Collections.emptyMap()));
131158
}
132159

133-
private ClassDesc getJavaStringType(String jsonSchemaFormat) {
134-
if (jsonSchemaFormat == null) {
135-
return CD_String;
136-
}
137-
return switch (jsonSchemaFormat) {
138-
case "date-time" -> of(OffsetDateTime.class.getName());
139-
case "time" -> of(OffsetTime.class.getName());
140-
case "date" -> of(LocalDate.class.getName());
141-
case "duration" -> of(Duration.class.getName());
142-
143-
case "ipv4" -> of(Inet4Address.class.getName());
144-
case "ipv6" -> of(Inet6Address.class.getName());
145-
146-
case "uuid" -> of(UUID.class.getName());
147-
case "uri", "uri-reference", "iri", "iri-reference" -> of(URI.class.getName());
148-
// "email", "idn-email", "hostname", "idn-hostname"
149-
default -> CD_String;
150-
};
151-
}
152-
153160
protected abstract Map<String, Object> loadModels(String jsonSchema);
154161

155162
protected Map<String, Object> loadModels(URI uri) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2024 Nacho Brito
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+
17+
package es.nachobrito.jsonschema.compiler.domain.schemareader;
18+
19+
import java.lang.constant.ClassDesc;
20+
import java.net.Inet4Address;
21+
import java.net.Inet6Address;
22+
import java.net.URI;
23+
import java.time.Duration;
24+
import java.time.LocalDate;
25+
import java.time.OffsetDateTime;
26+
import java.time.OffsetTime;
27+
import java.util.UUID;
28+
29+
import static java.lang.constant.ClassDesc.of;
30+
import static java.lang.constant.ConstantDescs.CD_String;
31+
32+
public class StringFormat {
33+
public static ClassDesc toClassDesc(String jsonSchemaFormat) {
34+
if (jsonSchemaFormat == null) {
35+
return CD_String;
36+
}
37+
return switch (jsonSchemaFormat) {
38+
case "date-time" -> of(OffsetDateTime.class.getName());
39+
case "time" -> of(OffsetTime.class.getName());
40+
case "date" -> of(LocalDate.class.getName());
41+
case "duration" -> of(Duration.class.getName());
42+
43+
case "ipv4" -> of(Inet4Address.class.getName());
44+
case "ipv6" -> of(Inet6Address.class.getName());
45+
46+
case "uuid" -> of(UUID.class.getName());
47+
case "uri", "uri-reference", "iri", "iri-reference" -> of(URI.class.getName());
48+
// "email", "idn-email", "hostname", "idn-hostname"
49+
default -> CD_String;
50+
};
51+
}
52+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2024 Nacho Brito
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+
17+
package es.nachobrito.jsonschema.compiler;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
22+
import java.io.IOException;
23+
import org.junit.jupiter.api.DisplayName;
24+
import org.junit.jupiter.api.Test;
25+
26+
public class ArraysTest extends CompilerTest {
27+
28+
@DisplayName("Array field types are treated as native arrays")
29+
@Test
30+
void expectArrayTypesHandledProperly()
31+
throws IOException, ClassNotFoundException, NoSuchFieldException {
32+
var cls = compileSampleSchemaFromFile("classpath:test-schemas/Arrays.json", "Product");
33+
assertNotNull(cls);
34+
35+
assertEquals(Double[].class, cls.getDeclaredField("references").getType());
36+
assertEquals(String[].class, cls.getDeclaredField("names").getType());
37+
assertEquals(Object[].class, cls.getDeclaredField("address").getType());
38+
39+
var itemsCls = cls.getDeclaredField("vegetables").getType().componentType();
40+
assertEquals(String.class, itemsCls.getDeclaredField("veggieName").getType());
41+
assertEquals(Boolean.class, itemsCls.getDeclaredField("veggieLike").getType());
42+
43+
}
44+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"$id": "https://example.com/person.schema.json",
3+
"$schema": "https://json-schema.org/draft/2020-12/schema",
4+
"title": "Product",
5+
"type": "object",
6+
"properties": {
7+
"references": {
8+
"type": "array",
9+
"items": {
10+
"type": "number"
11+
}
12+
},
13+
"names": {
14+
"type": "array",
15+
"items": {
16+
"type": "string"
17+
}
18+
},
19+
"address": {
20+
"type": "array",
21+
"prefixItems": [
22+
{ "type": "number" },
23+
{ "type": "string" },
24+
{ "enum": ["Street", "Avenue", "Boulevard"] },
25+
{ "enum": ["NW", "NE", "SW", "SE"] }
26+
]
27+
},
28+
"vegetables": {
29+
"type": "array",
30+
"items": { "$ref": "#/$defs/veggie" }
31+
}
32+
},
33+
"$defs": {
34+
"veggie": {
35+
"type": "object",
36+
"required": [
37+
"veggieName",
38+
"veggieLike"
39+
],
40+
"properties": {
41+
"veggieName": {
42+
"type": "string",
43+
"description": "The name of the vegetable."
44+
},
45+
"veggieLike": {
46+
"type": "boolean",
47+
"description": "Do I like this vegetable?"
48+
}
49+
}
50+
}
51+
}
52+
}
53+

0 commit comments

Comments
 (0)