diff --git a/README.md b/README.md index 011d32ef..c2428660 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,11 @@ Users are encouraged to use more up-to-date JSON Schema support tools. (from [TestGenerateJsonSchema](https://github.com/FasterXML/jackson-module-jsonSchema/blob/master/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestGenerateJsonSchema.java#L136)) -simply add a dependency (this is from my gradle config) -`"com.fasterxml.jackson.module:jackson-module-jsonSchema:2.9.0"` +simply add a dependency +`"com.fasterxml.jackson.module:jackson-module-jsonSchema:2.15.0"` +or +`"com.fasterxml.jackson.module:jackson-module-jsonSchema-jakarta:2.15.0"` for the jakarta namespace + and for gradle, at least, you can simply add `mavenLocal()` to your repositories. Maven should resolve the dependency from its local repo transparently. diff --git a/jakarta/pom.xml b/jakarta/pom.xml new file mode 100644 index 00000000..37047019 --- /dev/null +++ b/jakarta/pom.xml @@ -0,0 +1,55 @@ + + + + + + + 4.0.0 + + com.fasterxml.jackson.module + jackson-module-jsonSchema-parent + 2.15.0-SNAPSHOT + + jackson-module-jsonSchema-jakarta + jackson-module-jsonSchema-jakarta + bundle + Add-on module for Jackson (http://jackson.codehaus.org) to support +JSON Schema (http://tools.ietf.org/html/draft-zyp-json-schema-03) version 3 generation. + + + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + + com/fasterxml/jackson/module/jsonSchema/jakarta/failing/*.java + + + + + + org.moditect + moditect-maven-plugin + + + + de.jjohannes + gradle-module-metadata-maven-plugin + + + + diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchema.java new file mode 100644 index 00000000..35d94dc9 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchema.java @@ -0,0 +1,577 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.AnySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.BooleanSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ContainerTypeSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.IntegerSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NullSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.SimpleTypeSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.UnionTypeSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ValueTypeSchema; + +/** + * The type wraps the json schema specification at : + * Json JsonSchema + * Draft
JSON (JavaScript Object Notation) JsonSchema defines the + * media type "application/schema+json", a JSON based format for defining the + * structure of JSON data. JSON JsonSchema provides a contract for what JSON data is + * required for a given application and how to interact with it. JSON JsonSchema is + * intended to define validation, documentation, hyperlink navigation, and + * interaction control of JSON data.
+ * + *
JSON (JavaScript Object Notation) JsonSchema is a JSON media type + * for defining the structure of JSON data. JSON JsonSchema provides a contract for + * what JSON data is required for a given application and how to interact with + * it. JSON JsonSchema is intended to define validation, documentation, hyperlink + * navigation, and interaction control of JSON data.
+ * + * An example JSON JsonSchema provided by the JsonSchema draft: + * + *
+ * 	{
+ * 	  "name":"Product",
+ * 	  "properties":{
+ * 	    "id":{
+ * 	      "type":"number",
+ * 	      "description":"Product identifier",
+ * 	      "required":true
+ * 	    },
+ * 	    "name":{
+ * 	      "description":"Name of the product",
+ * 	      "type":"string",
+ * 	      "required":true
+ * 	    },
+ * 	    "price":{
+ * 	      "required":true,
+ * 	      "type": "number",
+ * 	      "minimum":0,
+ * 	      "required":true
+ * 	    },
+ * 	    "tags":{
+ * 	      "type":"array",
+ * 	      "items":{
+ * 	        "type":"string"
+ * 	      }
+ * 	    }
+ * 	  },
+ * 	  "links":[
+ * 	    {
+ * 	      "rel":"full",
+ * 	      "href":"{id}"
+ * 	    },
+ * 	    {
+ * 	      "rel":"comments",
+ * 	      "href":"comments/?id={id}"
+ * 	    }
+ * 	  ]
+ * 	}
+ * 
+ * + * @author jphelan + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonTypeInfo(use = Id.CUSTOM, include = As.PROPERTY, property = "type") +@JsonTypeIdResolver(JsonSchemaIdResolver.class) +public abstract class JsonSchema +{ + /** + * This attribute defines the current URI of this schema (this attribute is + * effectively a "self" link). This URI MAY be relative or absolute. If the + * URI is relative it is resolved against the current URI of the parent + * schema it is contained in. If this schema is not contained in any parent + * schema, the current URI of the parent schema is held to be the URI under + * which this schema was addressed. If id is missing, the current URI of a + * schema is defined to be that of the parent schema. The current URI of the + * schema is also used to construct relative references such as for $ref. + */ + @JsonProperty + private String id; + + /** + * This attribute defines a URI of a schema that contains the full + * representation of this schema. When a validator encounters this + * attribute, it SHOULD replace the current schema with the schema + * referenced by the value's URI (if known and available) and re- validate + * the instance. This URI MAY be relative or absolute, and relative URIs + * SHOULD be resolved against the URI of the current schema. + */ + @JsonProperty + private String $ref; + + /** + * This attribute defines a URI of a JSON JsonSchema that is the schema of the + * current schema. When this attribute is defined, a validator SHOULD use + * the schema referenced by the value's URI (if known and available) when + * resolving Hyper JsonSchema (Section 6) links (Section 6.1). + * + * A validator MAY use this attribute's value to determine which version of + * JSON JsonSchema the current schema is written in, and provide the appropriate + * validation features and behavior. Therefore, it is RECOMMENDED that all + * schema authors include this attribute in their schemas to prevent + * conflicts with future JSON JsonSchema specification changes. + */ + @JsonProperty + private String $schema; + + /** + * This attribute takes the same values as the "type" attribute, however if + * the instance matches the type or if this value is an array and the + * instance matches any type or schema in the array, then this instance is + * not valid. + */ + @JsonProperty + private JsonSchema[] disallow; + + /** + * The value of this property MUST be another schema which will provide a + * base schema which the current schema will inherit from. The inheritance + * rules are such that any instance that is valid according to the current + * schema MUST be valid according to the referenced schema. This MAY also be + * an array, in which case, the instance MUST be valid for all the schemas + * in the array. A schema that extends another schema MAY define additional + * attributes, constrain existing attributes, or add other constraints. + * + * Conceptually, the behavior of extends can be seen as validating an + * instance against all constraints in the extending schema as well as the + * extended schema(s). More optimized implementations that merge schemas are + * possible, but are not required. An example of using "extends": + * + * { "description":"An adult", "properties":{"age":{"minimum": 21}}, + * "extends":"person" } { "description":"Extended schema", + * "properties":{"deprecated":{"type": "boolean"}}, + * "extends":"http://json-schema.org/draft-03/schema" } + */ + private JsonSchema[] extendsextends; + + /** + * This attribute indicates if the instance must have a value, and not be + * undefined. This is false by default, making the instance optional. + */ + @JsonProperty + private Boolean required = null; + + /** + * This attribute indicates if the instance is not modifiable. + * This is false by default, making the instance modifiable. + */ + @JsonProperty + private Boolean readonly = null; + + /** + * This attribute is a string that provides a full description of the of + * purpose the instance property. + */ + private String description; + + protected JsonSchema() { } + + /** + * Attempt to return this JsonSchema as an {@link AnySchema} + * @return this as an AnySchema if possible, or null otherwise + */ + public AnySchema asAnySchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as an {@link ArraySchema} + * @return this as an ArraySchema if possible, or null otherwise + */ + public ArraySchema asArraySchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as a {@link BooleanSchema} + * @return this as a BooleanSchema if possible, or null otherwise + */ + public BooleanSchema asBooleanSchema() { + return null; + } + + /** + * @deprecated Since 2.7 + */ + @Deprecated + public ContainerTypeSchema asContainerSchema() { + return asContainerTypeSchema(); + } + + /** + * Attempt to return this JsonSchema as a {@link ContainerTypeSchema} + * @return this as an ContainerTypeSchema if possible, or null otherwise + * + * @since 2.7 + */ + public ContainerTypeSchema asContainerTypeSchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as an {@link IntegerSchema} + * @return this as an IntegerSchema if possible, or null otherwise + */ + public IntegerSchema asIntegerSchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as a {@link NullSchema} + * @return this as a NullSchema if possible, or null otherwise + */ + public NullSchema asNullSchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as a {@link NumberSchema} + * @return this as a NumberSchema if possible, or null otherwise + */ + public NumberSchema asNumberSchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as an {@link ObjectSchema} + * @return this as an ObjectSchema if possible, or null otherwise + */ + public ObjectSchema asObjectSchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as a {@link SimpleTypeSchema} + * @return this as a SimpleTypeSchema if possible, or null otherwise + */ + public SimpleTypeSchema asSimpleTypeSchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as a {@link StringSchema} + * @return this as a StringSchema if possible, or null otherwise + */ + public StringSchema asStringSchema() { + return null; + } + + /** + * Attempt to return this JsonSchema as an {@link UnionTypeSchema} + * @return this as a UnionTypeSchema if possible, or null otherwise + */ + public UnionTypeSchema asUnionTypeSchema() { + return null; + } + + /** + * @deprecated Since 2.7 + */ + @Deprecated + public ValueTypeSchema asValueSchemaSchema() { + return asValueTypeSchema(); + } + + /** + * Attempt to return this JsonSchema as a {@link ValueTypeSchema} + * @return this as a ValueTypeSchema if possible, or null otherwise + * + * @since 2.7 + */ + public ValueTypeSchema asValueTypeSchema() { + return null; + } + + public String getId() { + return id; + } + + public String get$ref() { + return $ref; + } + + public String get$schema() { + return $schema; + } + + public JsonSchema[] getDisallow() { + return disallow; + } + + public JsonSchema[] getExtends() { + return extendsextends; + } + + public Boolean getRequired() { + return required; + } + + public Boolean getReadonly() { + return readonly; + } + + public String getDescription() { + return description; + } + + @JsonIgnore + public abstract JsonFormatTypes getType(); + + /** + * determine if this JsonSchema is an {@link AnySchema}. + * + * @return true if this JsonSchema is an AnySchema, false otherwise + */ + @JsonIgnore + public boolean isAnySchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link ArraySchema}. + * + * @return true if this JsonSchema is an ArraySchema, false otherwise + */ + @JsonIgnore + public boolean isArraySchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link BooleanSchema}. + * + * @return true if this JsonSchema is an BooleanSchema, false otherwise + */ + @JsonIgnore + public boolean isBooleanSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link ContainerTypeSchema}. + * + * @return true if this JsonSchema is an ContainerTypeSchema, false otherwise + */ + @JsonIgnore + public boolean isContainerTypeSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link IntegerSchema}. + * + * @return true if this JsonSchema is an IntegerSchema, false otherwise + */ + @JsonIgnore + public boolean isIntegerSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link NullSchema}. + * + * @return true if this JsonSchema is an NullSchema, false otherwise + */ + @JsonIgnore + public boolean isNullSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link NumberSchema}. + * + * @return true if this JsonSchema is an NumberSchema, false otherwise + */ + @JsonIgnore + public boolean isNumberSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link ObjectSchema}. + * + * @return true if this JsonSchema is an ObjectSchema, false otherwise + */ + @JsonIgnore + public boolean isObjectSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link SimpleTypeSchema}. + * + * @return true if this JsonSchema is an SimpleTypeSchema, false otherwise + */ + @JsonIgnore + public boolean isSimpleTypeSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link StringSchema}. + * + * @return true if this JsonSchema is an StringSchema, false otherwise + */ + @JsonIgnore + public boolean isStringSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link UnionTypeSchema}. + * + * @return true if this JsonSchema is an UnionTypeSchema, false otherwise + */ + @JsonIgnore + public boolean isUnionTypeSchema() { + return false; + } + + /** + * determine if this JsonSchema is an {@link ValueTypeSchema}. + * + * @return true if this JsonSchema is an ValueTypeSchema, false otherwise + */ + @JsonIgnore + public boolean isValueTypeSchema() { + return false; + } + + public void set$ref(String $ref) { + this.$ref = $ref; + } + + public void set$schema(String $schema) { + this.$schema = $schema; + } + + public void setDisallow(JsonSchema[] disallow) { + this.disallow = disallow; + } + + public void setExtends(JsonSchema[] extendsextends) { + this.extendsextends = extendsextends; + } + + public void setId(String id) { + this.id = id; + } + + public void setRequired(Boolean required) { + this.required = required; + } + + public void setReadonly(Boolean readonly){ + this.readonly = readonly; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * Override this to add information specific to the property of bean + * For example, bean validation annotations could be used to specify + * value constraints in the schema + * @param beanProperty + */ + public void enrichWithBeanProperty(BeanProperty beanProperty) { + setDescription(beanProperty.getMetadata().getDescription()); + } + + /** + * Create a schema which verifies only that an object is of the given format. + * @param format the format to expect + * @return the schema verifying the given format + */ + public static JsonSchema minimalForFormat(JsonFormatTypes format) + { + if (format != null) { + switch (format) { + case ARRAY: + return new ArraySchema(); + case OBJECT: + return new ObjectSchema(); + case BOOLEAN: + return new BooleanSchema(); + case INTEGER: + return new IntegerSchema(); + case NUMBER: + return new NumberSchema(); + case STRING: + return new StringSchema(); + case NULL: + return new NullSchema(); + case ANY: + default: + } + } + return new AnySchema(); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof JsonSchema)) return false; + return _equals((JsonSchema) obj); + } + + protected boolean _equals(JsonSchema that) + { + return equals(getId(), that.getId()) + // 27-Apr-2015, tatu: Should not need to check type explicitly + // && equals(getType(), getType()) + && equals(getRequired(), that.getRequired()) + && equals(getReadonly(), that.getReadonly()) + && equals(get$ref(), that.get$ref()) + && equals(get$schema(), that.get$schema()) + && arraysEqual(getDisallow(), that.getDisallow()) + && arraysEqual(getExtends(), that.getExtends()); + } + + /** + * A utility method allowing to easily chain calls to equals() on members + * without taking any risk regarding the ternary operator precedence. + * + * @return (object1 == null ? object2 == null : object1.equals(object2)) + */ + protected static boolean equals(Object object1, Object object2) { + if (object1 == null) { + return object2 == null; + } + return object1.equals(object2); + } + + protected static boolean arraysEqual(T[] arr1, T[] arr2) { + if (arr1 == null) { + return arr2 == null; + } + if (arr2 == null) { + return false; + } + final int len = arr1.length; + if (len != arr2.length) { + return false; + } + for (int i = 0; i < len; ++i) { + if (!equals(arr1[i], arr2[i])) { + return false; + } + } + return true; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchemaGenerator.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchemaGenerator.java new file mode 100644 index 00000000..9c64e8e5 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchemaGenerator.java @@ -0,0 +1,102 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.WrapperFactory; + +/** + * Convenience class that wraps JSON Schema generation functionality. + * + * @author tsaloranta + */ +public class JsonSchemaGenerator +{ + /** + * @deprecated Since 2.6 + */ + @Deprecated + protected final ObjectMapper _mapper; + + /** + * @since 2.6 + */ + protected final ObjectWriter _writer; + + private final WrapperFactory _wrapperFactory; + + /** + * @since 2.8.1 + */ + private final SchemaFactoryWrapper _visitor; + + public JsonSchemaGenerator(ObjectMapper mapper) { + this(mapper, (WrapperFactory) null); + } + + public JsonSchemaGenerator(ObjectMapper mapper, WrapperFactory wrapperFactory) { + _mapper = mapper; + _writer = mapper.writer(); + _wrapperFactory = (wrapperFactory == null) ? new WrapperFactory() : wrapperFactory; + _visitor = null; + } + + /** + * NOTE: resulting generator is NOT thread-safe, since typically {@link SchemaFactoryWrapper} + * being passed is not thread-safe. + * + * @since 2.8.1 + */ + public JsonSchemaGenerator(ObjectMapper mapper, SchemaFactoryWrapper visitor) { + this(mapper.writer(), visitor); + } + + /** + * @since 2.6 + */ + public JsonSchemaGenerator(ObjectWriter w) { + this(w, (WrapperFactory) null); + } + + /** + * @since 2.6 + */ + public JsonSchemaGenerator(ObjectWriter w, WrapperFactory wrapperFactory) { + _mapper = null; + _writer = w; + _wrapperFactory = (wrapperFactory == null) ? new WrapperFactory() : wrapperFactory; + _visitor = null; + } + + /** + * @since 2.8.1 + */ + public JsonSchemaGenerator(ObjectWriter w, SchemaFactoryWrapper visitor) { + _mapper = null; + _writer = w; + _wrapperFactory = null; + if (visitor == null) { + throw new IllegalArgumentException("Missing `visitor`"); + } + _visitor = visitor; + } + + public JsonSchema generateSchema(Class type) throws JsonMappingException + { + SchemaFactoryWrapper visitor = _visitor; + if (visitor == null) { + visitor = _wrapperFactory.getWrapper(null); + } + _writer.acceptJsonFormatVisitor(type, visitor); + return visitor.finalSchema(); + } + + public JsonSchema generateSchema(JavaType type) throws JsonMappingException + { + SchemaFactoryWrapper visitor = _visitor; + if (visitor == null) { + visitor = _wrapperFactory.getWrapper(null); + } + _writer.acceptJsonFormatVisitor(type, visitor); + return visitor.finalSchema(); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchemaIdResolver.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchemaIdResolver.java new file mode 100644 index 00000000..4539dd73 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/JsonSchemaIdResolver.java @@ -0,0 +1,86 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.AnySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.BooleanSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.IntegerSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NullSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; +import java.util.Arrays; + +/** + * Type id resolver needed to support polymorphic (de)serialization of all kinds of + * {@link JsonSchema} instances. + * Note that to support custom types, you will need to sub-class this resolver + * and override at least {@link #idFromValue(Object)}, {@link #idFromValueAndType(Object, Class)} and + * {@link #typeFromId} methods; as well as associate this resolver using + * {@link com.fasterxml.jackson.annotation.JsonTypeInfo} annotation on + * all custom {@link JsonSchema} implementation classes. + */ +public class JsonSchemaIdResolver extends TypeIdResolverBase +{ + public JsonSchemaIdResolver() { } + + @Override + public String idFromValue(Object value) { + if (value instanceof JsonSchema) { + return ((JsonSchema)value).getType().value(); + } + return null; + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) { + return idFromValue(value); + } + + @Override + public JavaType typeFromId(DatabindContext ctxt, String id) + { + JsonFormatTypes stdType = JsonFormatTypes.forValue(id); + if (stdType != null) { + switch (stdType) { + case ARRAY: + return ctxt.constructType(ArraySchema.class); + case BOOLEAN: + return ctxt.constructType(BooleanSchema.class); + case INTEGER: + return ctxt.constructType(IntegerSchema.class); + case NULL: + return ctxt.constructType(NullSchema.class); + case NUMBER: + return ctxt.constructType(NumberSchema.class); + case OBJECT: + return ctxt.constructType(ObjectSchema.class); + case STRING: + return ctxt.constructType(StringSchema.class); + case ANY: + default: + return ctxt.constructType(AnySchema.class); + } + } + // Not a standard type; should use a custom sub-type impl + throw new IllegalArgumentException("Can not resolve JsonSchema 'type' id of \""+id + +"\", not recognized as one of standard values: "+Arrays.asList(JsonFormatTypes.values())); + } + + @Override + public Id getMechanism() { + return Id.CUSTOM; + } + + @Override + public void init(JavaType baseType) { } + + @Override + public String idFromBaseType() { + return null; + } + } diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/annotation/JsonHyperSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/annotation/JsonHyperSchema.java new file mode 100644 index 00000000..3ba65033 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/annotation/JsonHyperSchema.java @@ -0,0 +1,95 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.annotation; + +import java.lang.annotation.*; + +/** + * Created by mavarazy on 4/21/14. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface JsonHyperSchema { + + /** + * This attribute is a URI that defines what the instance's URI MUST + start with in order to validate. The value of the "pathStart" + attribute MUST be resolved as per RFC 3986, Sec 5 [RFC3986], and is + relative to the instance's URI. + + When multiple schemas have been referenced for an instance, the user + agent can determine if this jsonSchema is applicable for a particular + instance by determining if the URI of the instance begins with the + the value of the "pathStart" attribute. If the URI of the instance + does not start with this URI, or if another jsonSchema specifies a + starting URI that is longer and also matches the instance, this + jsonSchema SHOULD NOT be applied to the instance. Any jsonSchema that does + not have a pathStart attribute SHOULD be considered applicable to all + the instances for which it is referenced. + */ + String pathStart() default ""; + + /** + * 6.2.1. slash-delimited fragment resolution + + With the slash-delimited fragment resolution protocol, the fragment + identifier is interpreted as a series of property reference tokens + that start with and are delimited by the "/" character (\x2F). Each + property reference token is a series of unreserved or escaped URI + characters. Each property reference token SHOULD be interpreted, + starting from the beginning of the fragment identifier, as a path + reference in the target JSON structure. The final target value of + the fragment can be determined by starting with the root of the JSON + structure from the representation of the resource identified by the + pre-fragment URI. If the target is a JSON object, then the new + target is the value of the property with the name identified by the + next property reference token in the fragment. If the target is a + JSON array, then the target is determined by finding the item in + array the array with the index defined by the next property reference + token (which MUST be a number). The target is successively updated + for each property reference token, until the entire fragment has been + traversed. + + Property names SHOULD be URI-encoded. In particular, any "/" in a + property name MUST be encoded to avoid being interpreted as a + property delimiter. + + For example, for the following JSON representation: + + { + "foo":{ + "anArray":[ + {"prop":44} + ], + "another prop":{ + "baz":"A string" + } + } + } + + The following fragment identifiers would be resolved: + + fragment identifier resolution + ------------------- ---------- + # self, the root of the resource itself + #/foo the object referred to by the foo property + #/foo/another%20prop the object referred to by the "another prop" + property of the object referred to by the + "foo" property + #/foo/another%20prop/baz the string referred to by the value of "baz" + property of the "another prop" property of + the object referred to by the "foo" property + #/foo/anArray/0 the first object in the "anArray" array + + 6.2.2. dot-delimited fragment resolution + + The dot-delimited fragment resolution protocol is the same as slash- + delimited fragment resolution protocol except that the "." character + (\x2E) is used as the delimiter between property names (instead of + "/") and the path does not need to start with a ".". For example, + #.foo and #foo are a valid fragment identifiers for referencing the + value of the foo propery. + */ + + Link[] links() default {}; + +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/annotation/Link.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/annotation/Link.java new file mode 100644 index 00000000..3ca03db9 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/annotation/Link.java @@ -0,0 +1,209 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.annotation; + +/** + * A link description object is used to describe link relations. In the + context of a jsonSchema, it defines the link relations of the instances + of the jsonSchema, and can be parameterized by the instance values. The + link description format can be used on its own in regular (non-jsonSchema + documents), and use of this format can be declared by referencing the + normative link description jsonSchema as the the jsonSchema for the data + structure that uses the links. + */ +public @interface Link { + + /** + * The value of the "href" link description property indicates the + target URI of the related resource. The value of the instance + property SHOULD be resolved as a URI-Reference per RFC 3986 [RFC3986] + and MAY be a relative URI. The base URI to be used for relative + resolution SHOULD be the URI used to retrieve the instance object + (not the jsonSchema) when used within a jsonSchema. Also, when links are + used within a jsonSchema, the URI SHOULD be parametrized by the property + values of the instance object, if property values exist for the + corresponding variables in the template (otherwise they MAY be + provided from alternate sources, like user input). + + Instance property values SHOULD be substituted into the URIs where + matching braces ('{', '}') are found surrounding zero or more + characters, creating an expanded URI. Instance property value + substitutions are resolved by using the text between the braces to + denote the property name from the instance to get the value to + substitute. For example, if an href value is defined: + + http://somesite.com/{id} + + Then it would be resolved by replace the value of the "id" property + value from the instance object. If the value of the "id" property + was "45", the expanded URI would be: + + http://somesite.com/45 + + If matching braces are found with the string "@" (no quotes) between + the braces, then the actual instance value SHOULD be used to replace + the braces, rather than a property value. This should only be used + in situations where the instance is a scalar (string, boolean, or + number), and not for objects or arrays. + + */ + String href(); + + /** + * The value of the "rel" property indicates the name of the relation to + the target resource. The relation to the target SHOULD be + interpreted as specifically from the instance object that the jsonSchema + (or sub-jsonSchema) applies to, not just the top level resource that + contains the object within its hierarchy. If a resource JSON + representation contains a sub object with a property interpreted as a + link, that sub-object holds the relation with the target. A relation + to target from the top level resource MUST be indicated with the + jsonSchema describing the top level JSON representation. + + Relationship definitions SHOULD NOT be media type dependent, and + users are encouraged to utilize existing accepted relation + definitions, including those in existing relation registries (see RFC + 4287 [RFC4287]). However, we define these relations here for clarity + of normative interpretation within the context of JSON hyper jsonSchema + defined relations: + + self If the relation value is "self", when this property is + encountered in the instance object, the object represents a + resource and the instance object is treated as a full + representation of the target resource identified by the specified + URI. + + full This indicates that the target of the link is the full + representation for the instance object. The object that contains + this link possibly may not be the full representation. + + describedby This indicates the target of the link is the jsonSchema for + the instance object. This MAY be used to specifically denote the + schemas of objects within a JSON object hierarchy, facilitating + polymorphic type data structures. + + root This relation indicates that the target of the link SHOULD be + treated as the root or the body of the representation for the + purposes of user agent interaction or fragment resolution. All + other properties of the instance objects can be regarded as meta- + data descriptions for the data. + + The following relations are applicable for schemas (the jsonSchema as the + "from" resource in the relation): + + instances This indicates the target resource that represents + collection of instances of a jsonSchema. + + create This indicates a target to use for creating new instances of + a jsonSchema. This link definition SHOULD be a submission link with a + non-safe method (like POST). + + For example, if a jsonSchema is defined: + + { + "links": [ + { + "rel": "self" + "href": "{id}" + }, + { + "rel": "up" + "href": "{upId}" + }, + { + "rel": "children" + "href": "?upId={id}" + } + ] + } + + And if a collection of instance resource's JSON representation was + retrieved: + + GET /Resource/ + + [ + { + "id": "thing", + "upId": "parent" + }, + { + "id": "thing2", + "upId": "parent" + } + ] + + This would indicate that for the first item in the collection, its + own (self) URI would resolve to "/Resource/thing" and the first + item's "up" relation SHOULD be resolved to the resource at + "/Resource/parent". The "children" collection would be located at + "/Resource/?upId=thing". + */ + String rel(); + + + /** + * This property value is a jsonSchema that defines the expected structure + of the JSON representation of the target of the link. + */ + Class targetSchema() default void.class; + + /** + * This attribute defines which method can be used to access the target + resource. In an HTTP environment, this would be "GET" or "POST" + (other HTTP methods such as "PUT" and "DELETE" have semantics that + are clearly implied by accessed resources, and do not need to be + defined here). This defaults to "GET". + */ + String method() default "GET"; + + /** + * If present, this property indicates a query media type format that + the server supports for querying or posting to the collection of + instances at the target resource. The query can be suffixed to the + target URI to query the collection with property-based constraints on + the resources that SHOULD be returned from the server or used to post + data to the resource (depending on the method). For example, with + the following jsonSchema: + + { + "links":[ + { + "enctype":"application/x-www-form-urlencoded", + "method":"GET", + "href":"/Product/", + "properties":{ + "name":{"description":"name of the product"} + } + } + ] + } + This indicates that the client can query the server for instances + that have a specific name: + + /Product/?name=Slinky + + If no enctype or method is specified, only the single URI specified + by the href property is defined. If the method is POST, + "application/json" is the default media type. + */ + String enctype() default "application/json"; + + /** + * This attribute contains a jsonSchema which defines the acceptable + structure of the submitted request (for a GET request, this jsonSchema + would define the properties for the query string and for a POST + request, this would define the body). + */ + Class schema() default void.class; + + /** + * This property defines a title for the link. The value must be a string. + User agents MAY use this title when presenting the link to the user. + */ + String title() default ""; + + /** + * The value of this property is advisory only, and represents the media type RFC 2046 [RFC2046], + that is expected to be returned when fetching this resource. + */ + String mediaType() default "application/json"; +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/HyperSchemaFactoryWrapper.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/HyperSchemaFactoryWrapper.java new file mode 100644 index 00000000..47931270 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/HyperSchemaFactoryWrapper.java @@ -0,0 +1,134 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.customProperties; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.ArrayVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.ObjectVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.VisitorContext; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.WrapperFactory; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.LinkDescriptionObject; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ReferenceSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.SimpleTypeSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.annotation.JsonHyperSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.annotation.Link; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.*; + +/** + * Adds a hyperlink to object schema, either root level or nested. Generally + * useful for writing additional properties to a schema. + * + * @author mavarazy + */ +public class HyperSchemaFactoryWrapper extends SchemaFactoryWrapper { + + private boolean ignoreDefaults = true; + + private static class HyperSchemaFactoryWrapperFactory extends WrapperFactory { + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p) { + return new HyperSchemaFactoryWrapper(p); + }; + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p, VisitorContext rvc) + { + return new HyperSchemaFactoryWrapper(p) + .setVisitorContext(rvc); + } + }; + + public HyperSchemaFactoryWrapper() { + super(new HyperSchemaFactoryWrapperFactory()); + } + + public HyperSchemaFactoryWrapper(SerializerProvider p) { + super(p, new HyperSchemaFactoryWrapperFactory()); + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { + ObjectVisitor visitor = ((ObjectVisitor)super.expectObjectFormat(convertedType)); + + // could add other properties here + addHyperlinks(visitor.getSchema(), convertedType); + + return visitor; + } + + @Override + public JsonArrayFormatVisitor expectArrayFormat(JavaType convertedType) { + ArrayVisitor visitor = ((ArrayVisitor)super.expectArrayFormat(convertedType)); + + // could add other properties here + addHyperlinks(visitor.getSchema(), convertedType); + + return visitor; + } + + public void setIgnoreDefaults(boolean ignoreDefaults) { + this.ignoreDefaults = ignoreDefaults; + } + + /** + * Adds writes the type as the title of the schema. + * + * @param schema The schema who's title to set. + * @param type The type of the object represented by the schema. + */ + private void addHyperlinks(JsonSchema schema, JavaType type) { + if (!schema.isSimpleTypeSchema()) { + throw new RuntimeException("given non simple type schema: " + schema.getType()); + } + Class rawClass = type.getRawClass(); + if (rawClass.isAnnotationPresent(JsonHyperSchema.class)) { + JsonHyperSchema hyperSchema = rawClass.getAnnotation(JsonHyperSchema.class); + String pathStart = hyperSchema.pathStart(); + Link[] links = hyperSchema.links(); + LinkDescriptionObject[] linkDescriptionObjects = new LinkDescriptionObject[links.length]; + for(int i = 0; i < links.length; i++) { + Link link = links[i]; + linkDescriptionObjects[i] = new LinkDescriptionObject() + .setHref(pathStart + link.href()) + .setRel(link.rel()) + .setMethod(ignoreDefaults && "GET".equals(link.method()) ? null : link.method()) + .setEnctype(ignoreDefaults && "application/json".equals(link.enctype()) ? null : link.enctype()) + .setTargetSchema(fetchSchema(link.targetSchema())) + .setSchema(fetchSchema(link.schema())) + .setMediaType(ignoreDefaults && "application/json".equals(link.mediaType()) ? null : link.mediaType()) + .setTitle(link.title()); + } + SimpleTypeSchema simpleTypeSchema = schema.asSimpleTypeSchema(); + simpleTypeSchema.setLinks(linkDescriptionObjects); + if(pathStart != null && pathStart.length() != 0) + simpleTypeSchema.setPathStart(pathStart); + } + } + + private JsonSchema fetchSchema(Class targetSchema) { + if (provider instanceof DefaultSerializerProvider && targetSchema != void.class) { + JavaType targetType = provider.constructType(targetSchema); + try { + if (visitorContext != null) { + String seenSchemaUri = visitorContext.getSeenSchemaUri(targetType); + if (seenSchemaUri != null) { + return new ReferenceSchema(seenSchemaUri); + } + } + HyperSchemaFactoryWrapper targetVisitor = new HyperSchemaFactoryWrapper(); + targetVisitor.setVisitorContext(visitorContext); + + ((DefaultSerializerProvider) provider).acceptJsonFormatVisitor(targetType, targetVisitor); + return targetVisitor.finalSchema(); + } catch (JsonMappingException e) { + throw new RuntimeException(e); + } + } + return null; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/TitleSchemaFactoryWrapper.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/TitleSchemaFactoryWrapper.java new file mode 100644 index 00000000..734df74a --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/TitleSchemaFactoryWrapper.java @@ -0,0 +1,81 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.customProperties; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.ArrayVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.ObjectVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.VisitorContext; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.WrapperFactory; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.*; + +/** + * Adds a title to every object schema, either root level or nested. Generally + * useful for writing additional properties to a schema. + * + * @author jphelan + */ +public class TitleSchemaFactoryWrapper extends SchemaFactoryWrapper +{ + private static class TitleSchemaFactoryWrapperFactory extends WrapperFactory { + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p) { + SchemaFactoryWrapper wrapper = new TitleSchemaFactoryWrapper(); + if (p != null) { + wrapper.setProvider(p); + } + return wrapper; + }; + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p, VisitorContext rvc) { + SchemaFactoryWrapper wrapper = new TitleSchemaFactoryWrapper(); + if (p != null) { + wrapper.setProvider(p); + } + wrapper.setVisitorContext(rvc); + return wrapper; + } + }; + + public TitleSchemaFactoryWrapper() { + super(new TitleSchemaFactoryWrapperFactory()); + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { + ObjectVisitor visitor = ((ObjectVisitor)super.expectObjectFormat(convertedType)); + + // could add other properties here + addTitle(visitor.getSchema(), convertedType); + + return visitor; + } + + @Override + public JsonArrayFormatVisitor expectArrayFormat(JavaType convertedType) { + ArrayVisitor visitor = ((ArrayVisitor)super.expectArrayFormat(convertedType)); + + // could add other properties here + addTitle(visitor.getSchema(), convertedType); + + return visitor; + } + + /** + * Adds writes the type as the title of the schema. + * + * @param schema The schema who's title to set. + * @param type The type of the object represented by the schema. + */ + private void addTitle(JsonSchema schema, JavaType type) + { + if (!schema.isSimpleTypeSchema()) { + throw new RuntimeException("given non simple type schema: " + schema.getType()); + } + schema.asSimpleTypeSchema().setTitle(type.getGenericSignature()); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/ValidationSchemaFactoryWrapper.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/ValidationSchemaFactoryWrapper.java new file mode 100644 index 00000000..7d12a96e --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/customProperties/ValidationSchemaFactoryWrapper.java @@ -0,0 +1,100 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.customProperties; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.ObjectVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.ObjectVisitorDecorator; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.VisitorContext; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.WrapperFactory; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.*; +import com.fasterxml.jackson.module.jsonSchema.jakarta.validation.AnnotationConstraintResolver; +import com.fasterxml.jackson.module.jsonSchema.jakarta.validation.ValidationConstraintResolver; + +/** + * @author cponomaryov + */ +public class ValidationSchemaFactoryWrapper extends SchemaFactoryWrapper { + + private ValidationConstraintResolver constraintResolver; + + private static class ValidationSchemaFactoryWrapperFactory extends WrapperFactory { + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p) { + SchemaFactoryWrapper wrapper = new ValidationSchemaFactoryWrapper(); + wrapper.setProvider(p); + return wrapper; + } + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p, VisitorContext rvc) { + SchemaFactoryWrapper wrapper = new ValidationSchemaFactoryWrapper(); + wrapper.setProvider(p); + wrapper.setVisitorContext(rvc); + return wrapper; + } + } + + public ValidationSchemaFactoryWrapper() { + this(new AnnotationConstraintResolver()); + } + + public ValidationSchemaFactoryWrapper(ValidationConstraintResolver constraintResolver) { + super(new ValidationSchemaFactoryWrapperFactory()); + this.constraintResolver = constraintResolver; + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { + return new ObjectVisitorDecorator((ObjectVisitor) super.expectObjectFormat(convertedType)) { + private JsonSchema getPropertySchema(BeanProperty writer) { + return ((ObjectSchema) getSchema()).getProperties().get(writer.getName()); + } + + @Override + public void optionalProperty(BeanProperty writer) throws JsonMappingException { + super.optionalProperty(writer); + addValidationConstraints(getPropertySchema(writer), writer); + } + + @Override + public void property(BeanProperty writer) throws JsonMappingException { + super.property(writer); + addValidationConstraints(getPropertySchema(writer), writer); + } + }; + } + + protected JsonSchema addValidationConstraints(JsonSchema schema, BeanProperty prop) { + { + Boolean required = constraintResolver.getRequired(prop); + if (required != null) { + schema.setRequired(required); + } + } + if (schema.isArraySchema()) { + ArraySchema arraySchema = schema.asArraySchema(); + arraySchema.setMaxItems(constraintResolver.getArrayMaxItems(prop)); + arraySchema.setMinItems(constraintResolver.getArrayMinItems(prop)); + } else if (schema.isNumberSchema()) { + NumberSchema numberSchema = schema.asNumberSchema(); + numberSchema.setMaximum(constraintResolver.getNumberMaximum(prop)); + numberSchema.setMinimum(constraintResolver.getNumberMinimum(prop)); + } else if (schema.isStringSchema()) { + StringSchema stringSchema = schema.asStringSchema(); + stringSchema.setMaxLength(constraintResolver.getStringMaxLength(prop)); + stringSchema.setMinLength(constraintResolver.getStringMinLength(prop)); + stringSchema.setPattern(constraintResolver.getStringPattern(prop)); + } + return schema; + } + +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/AnyVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/AnyVisitor.java new file mode 100644 index 00000000..70a0d7d5 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/AnyVisitor.java @@ -0,0 +1,31 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonAnyFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.AnySchema; + +public class AnyVisitor extends JsonAnyFormatVisitor.Base + implements JsonSchemaProducer +{ + protected final AnySchema schema; + + public AnyVisitor(AnySchema schema) { + this.schema = schema; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public AnySchema getSchema() { + return schema; + } + + /* + /********************************************************************* + /* AnyVisitor: no additional methods... + /********************************************************************* + */ +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ArrayVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ArrayVisitor.java new file mode 100644 index 00000000..5345d535 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ArrayVisitor.java @@ -0,0 +1,100 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ReferenceSchema; + +public class ArrayVisitor extends JsonArrayFormatVisitor.Base + implements JsonSchemaProducer, Visitor +{ + protected final ArraySchema schema; + + protected SerializerProvider provider; + + private WrapperFactory wrapperFactory; + + private VisitorContext visitorContext; + + public ArrayVisitor(SerializerProvider provider, ArraySchema schema) { + this(provider, schema, new WrapperFactory()); + } + + public ArrayVisitor(SerializerProvider provider, ArraySchema schema, WrapperFactory wrapperFactory) { + this.provider = provider; + this.schema = schema; + this.wrapperFactory = wrapperFactory; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public JsonSchema getSchema() { + return schema; + } + + /* + /********************************************************************* + /* JsonArrayFormatVisitor + /********************************************************************* + */ + + @Override + public SerializerProvider getProvider() { + return provider; + } + + @Override + public void setProvider(SerializerProvider p) { + provider = p; + } + + public WrapperFactory getWrapperFactory() { + return wrapperFactory; + } + + public void setWrapperFactory(WrapperFactory wrapperFactory) { + this.wrapperFactory = wrapperFactory; + } + + @Override + public void itemsFormat(JsonFormatVisitable handler, JavaType contentType) + throws JsonMappingException + { + // An array of object matches any values, thus we leave the schema empty. + if (contentType.getRawClass() != Object.class) { + + // check if we've seen this sub-schema already and return a reference-schema if we have + if (visitorContext != null) { + String seenSchemaUri = visitorContext.getSeenSchemaUri(contentType); + if (seenSchemaUri != null) { + schema.setItemsSchema(new ReferenceSchema(seenSchemaUri)); + return; + } + } + + SchemaFactoryWrapper visitor = wrapperFactory.getWrapper(getProvider(), visitorContext); + handler.acceptJsonFormatVisitor(visitor, contentType); + schema.setItemsSchema(visitor.finalSchema()); + } + } + + @Override + public void itemsFormat(JsonFormatTypes format) throws JsonMappingException + { + schema.setItemsSchema(JsonSchema.minimalForFormat(format)); + } + + @Override + public Visitor setVisitorContext(VisitorContext rvc) { + visitorContext = rvc; + return this; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/BooleanVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/BooleanVisitor.java new file mode 100644 index 00000000..7c78fc0e --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/BooleanVisitor.java @@ -0,0 +1,44 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.BooleanSchema; +import java.util.Set; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonBooleanFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; + +public class BooleanVisitor extends JsonBooleanFormatVisitor.Base + implements JsonSchemaProducer +{ + protected final BooleanSchema schema; + + public BooleanVisitor(BooleanSchema schema) { + this.schema = schema; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public BooleanSchema getSchema() { + return schema; + } + + /* + /********************************************************************* + /* JsonBooleanFormatVisitor impl + /********************************************************************* + */ + + @Override + public void enumTypes(Set enums) { + schema.setEnums(enums); + } + + @Override + public void format(JsonValueFormat format) { + schema.setFormat(format); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/FormatVisitorFactory.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/FormatVisitorFactory.java new file mode 100644 index 00000000..d81ac3f6 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/FormatVisitorFactory.java @@ -0,0 +1,105 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.*; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.AnySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.BooleanSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.IntegerSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NullSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.*; + +/** + * Factory class used for constructing visitors for building various + * JSON Schema instances via visitor interface. + */ +public class FormatVisitorFactory { + + private final WrapperFactory wrapperFactory; + + public FormatVisitorFactory() { + this(new WrapperFactory()); + } + + public FormatVisitorFactory(WrapperFactory wrapperFactory) { + this.wrapperFactory = wrapperFactory; + } + + /* + /********************************************************** + /* Factory methods for visitors, structured types + /********************************************************** + */ + + public JsonAnyFormatVisitor anyFormatVisitor(AnySchema anySchema) { + return null; + } + + public JsonArrayFormatVisitor arrayFormatVisitor(SerializerProvider provider, + ArraySchema arraySchema) { + return new ArrayVisitor(provider, arraySchema, wrapperFactory); + } + + public JsonMapFormatVisitor mapFormatVisitor(SerializerProvider provider, + ObjectSchema objectSchema) { + return new MapVisitor(provider, objectSchema, wrapperFactory); + } + + public JsonObjectFormatVisitor objectFormatVisitor(SerializerProvider provider, + ObjectSchema objectSchema) { + return new ObjectVisitor(provider, objectSchema, wrapperFactory); + } + + + protected JsonArrayFormatVisitor arrayFormatVisitor(SerializerProvider provider, + ArraySchema arraySchema, VisitorContext rvc) { + ArrayVisitor v = new ArrayVisitor(provider, arraySchema, wrapperFactory); + v.setVisitorContext(rvc); + return v; + } + + protected JsonMapFormatVisitor mapFormatVisitor(SerializerProvider provider, + ObjectSchema objectSchema, VisitorContext rvc) { + MapVisitor v = new MapVisitor(provider, objectSchema, wrapperFactory); + v.setVisitorContext(rvc); + return v; + } + + protected JsonObjectFormatVisitor objectFormatVisitor(SerializerProvider provider, + ObjectSchema objectSchema, VisitorContext rvc) { + ObjectVisitor v = new ObjectVisitor(provider, objectSchema, wrapperFactory); + v.setVisitorContext(rvc); + return v; + } + + /* + /********************************************************** + /* Factory methods for visitors, value types + /********************************************************** + */ + + public JsonBooleanFormatVisitor booleanFormatVisitor(BooleanSchema booleanSchema) { + return new BooleanVisitor(booleanSchema); + } + + public JsonIntegerFormatVisitor integerFormatVisitor(IntegerSchema integerSchema) { + return new IntegerVisitor(integerSchema); + } + + // no ValueTypeSchemaFactory, since null type has no formatting + public JsonNullFormatVisitor nullFormatVisitor(NullSchema nullSchema) { + return new NullVisitor(nullSchema); + } + + public JsonNumberFormatVisitor numberFormatVisitor(NumberSchema numberSchema) { + return new NumberVisitor(numberSchema); + } + + public JsonStringFormatVisitor stringFormatVisitor(StringSchema stringSchema) { + return new StringVisitor(stringSchema); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/IntegerVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/IntegerVisitor.java new file mode 100644 index 00000000..c6d90329 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/IntegerVisitor.java @@ -0,0 +1,44 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import java.util.Set; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.IntegerSchema; + +public class IntegerVisitor extends JsonIntegerFormatVisitor.Base + implements JsonSchemaProducer +{ + protected final IntegerSchema schema; + + public IntegerVisitor(IntegerSchema schema) { + this.schema = schema; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public IntegerSchema getSchema() { + return schema; + } + + /* + /********************************************************************* + /* JsonIntegerFormatVisitor + /********************************************************************* + */ + + @Override + public void enumTypes(Set enums) { + schema.setEnums(enums); + } + + @Override + public void format(JsonValueFormat format) { + schema.setFormat(format); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/JsonSchemaFactory.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/JsonSchemaFactory.java new file mode 100644 index 00000000..38aeb25c --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/JsonSchemaFactory.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.AnySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.BooleanSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.IntegerSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NullSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; + +public class JsonSchemaFactory +{ + public AnySchema anySchema() { + return new AnySchema(); + } + + public ArraySchema arraySchema() { + return new ArraySchema(); + } + + public BooleanSchema booleanSchema() { + return new BooleanSchema(); + } + + public IntegerSchema integerSchema() { + return new IntegerSchema(); + } + + public NullSchema nullSchema() { + return new NullSchema(); + } + + public NumberSchema numberSchema() { + return new NumberSchema(); + } + + public ObjectSchema objectSchema() { + return new ObjectSchema(); + } + + public StringSchema stringSchema() { + return new StringSchema(); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/JsonSchemaProducer.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/JsonSchemaProducer.java new file mode 100644 index 00000000..6ef5e7ab --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/JsonSchemaProducer.java @@ -0,0 +1,13 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * Interface for objects that produce {@link JsonSchema} instances; + * implemented by visitors. + * + * @author jphelan + */ +public interface JsonSchemaProducer { + public JsonSchema getSchema(); +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/MapVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/MapVisitor.java new file mode 100644 index 00000000..af4f7281 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/MapVisitor.java @@ -0,0 +1,103 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ReferenceSchema; + +/** + * While JSON Schema does not have notion of "Map" type (unlimited property + * names), Jackson has, so the distinction is exposed. We will need + * to handle it here, produce JSON Schema Object type. + */ +public class MapVisitor extends JsonMapFormatVisitor.Base + implements JsonSchemaProducer, Visitor +{ + protected final ObjectSchema schema; + + protected SerializerProvider provider; + + private WrapperFactory wrapperFactory; + + private VisitorContext visitorContext; + + public MapVisitor(SerializerProvider provider, ObjectSchema schema) { + this(provider, schema, new WrapperFactory()); + } + + public MapVisitor(SerializerProvider provider, ObjectSchema schema, WrapperFactory wrapperFactory) { + this.provider = provider; + this.schema = schema; + this.wrapperFactory = wrapperFactory; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public ObjectSchema getSchema() { + return schema; + } + + /* + /********************************************************************* + /* JsonMapFormatVisitor + /********************************************************************* + */ + + @Override + public SerializerProvider getProvider() { + return provider; + } + + @Override + public void setProvider(SerializerProvider p) { + provider = p; + } + + @Override + public void keyFormat(JsonFormatVisitable handler, JavaType keyType) + throws JsonMappingException { + // JSON Schema only allows String types so let's not bother too much + } + + @Override + public void valueFormat(JsonFormatVisitable handler, JavaType valueType) + throws JsonMappingException { + + // ISSUE #24: https://github.com/FasterXML/jackson-module-jsonSchema/issues/24 + + JsonSchema valueSchema = propertySchema(handler, valueType); + ObjectSchema.AdditionalProperties ap = new ObjectSchema.SchemaAdditionalProperties(valueSchema.asSimpleTypeSchema()); + this.schema.setAdditionalProperties(ap); + } + + protected JsonSchema propertySchema(JsonFormatVisitable handler, JavaType propertyTypeHint) + throws JsonMappingException { + + // check if we've seen this sub-schema already and return a reference-schema if we have + if (visitorContext != null) { + String seenSchemaUri = visitorContext.getSeenSchemaUri(propertyTypeHint); + if (seenSchemaUri != null) { + return new ReferenceSchema(seenSchemaUri); + } + } + + SchemaFactoryWrapper visitor = wrapperFactory.getWrapper(getProvider(), visitorContext); + handler.acceptJsonFormatVisitor(visitor, propertyTypeHint); + return visitor.finalSchema(); + } + + @Override + public Visitor setVisitorContext(VisitorContext rvc) { + visitorContext = rvc; + return this; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/NullVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/NullVisitor.java new file mode 100644 index 00000000..e1d337f5 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/NullVisitor.java @@ -0,0 +1,19 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNullFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NullSchema; + +public class NullVisitor extends JsonNullFormatVisitor.Base + implements JsonSchemaProducer +{ + protected final NullSchema schema; + + public NullVisitor(NullSchema schema) { + this.schema = schema; + } + + @Override + public NullSchema getSchema() { + return schema; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/NumberVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/NumberVisitor.java new file mode 100644 index 00000000..1c1e81f0 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/NumberVisitor.java @@ -0,0 +1,44 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import java.util.Set; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; + +public class NumberVisitor extends JsonNumberFormatVisitor.Base + implements JsonSchemaProducer +{ + protected final NumberSchema schema; + + public NumberVisitor(NumberSchema schema) { + this.schema = schema; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public NumberSchema getSchema() { + return schema; + } + + /* + /********************************************************************* + /* JsonNumberFormatVisitor + /********************************************************************* + */ + + @Override + public void enumTypes(Set enums) { + schema.setEnums(enums); + } + + @Override + public void format(JsonValueFormat format) { + schema.setFormat(format); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ObjectVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ObjectVisitor.java new file mode 100644 index 00000000..851075f9 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ObjectVisitor.java @@ -0,0 +1,158 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ReferenceSchema; + +public class ObjectVisitor extends JsonObjectFormatVisitor.Base + implements JsonSchemaProducer, Visitor +{ + protected final ObjectSchema schema; + protected SerializerProvider provider; + private WrapperFactory wrapperFactory; + private VisitorContext visitorContext; + + /** + * @deprecated Since 2.4; call constructor that takes {@link WrapperFactory} + */ + @Deprecated + public ObjectVisitor(SerializerProvider provider, ObjectSchema schema) { + this(provider, schema, new WrapperFactory()); + } + + public ObjectVisitor(SerializerProvider provider, ObjectSchema schema, WrapperFactory wrapperFactory) { + this.provider = provider; + this.schema = schema; + this.wrapperFactory = wrapperFactory; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public ObjectSchema getSchema() { + return schema; + } + + /* + /********************************************************************* + /* JsonObjectFormatVisitor impl + /********************************************************************* + */ + + @Override + public SerializerProvider getProvider() { + return provider; + } + + /** + * @deprecated Construct instances with provider instead + */ + @Deprecated + @Override + public void setProvider(SerializerProvider p) { + provider = p; + } + + public WrapperFactory getWrapperFactory() { + return wrapperFactory; + } + + /** + * @deprecated Construct instances with provider instead + */ + @Deprecated + public void setWrapperFactory(WrapperFactory wrapperFactory) { + this.wrapperFactory = wrapperFactory; + } + + @Override + public void optionalProperty(BeanProperty prop) throws JsonMappingException { + schema.putOptionalProperty(prop, propertySchema(prop)); + } + + @Override + public void optionalProperty(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) + throws JsonMappingException { + schema.putOptionalProperty(name, propertySchema(handler, propertyTypeHint)); + } + + @Override + public void property(BeanProperty prop) throws JsonMappingException { + schema.putProperty(prop, propertySchema(prop)); + } + + @Override + public void property(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) + throws JsonMappingException { + schema.putProperty(name, propertySchema(handler, propertyTypeHint)); + } + + protected JsonSchema propertySchema(BeanProperty prop) + throws JsonMappingException + { + if (prop == null) { + throw new IllegalArgumentException("Null property"); + } + + // check if we've seen this argument's sub-schema already and return a reference-schema if we have + String seenSchemaUri = visitorContext.getSeenSchemaUri(prop.getType()); + if (seenSchemaUri != null) { + return new ReferenceSchema(seenSchemaUri); + } + + SchemaFactoryWrapper visitor = wrapperFactory.getWrapper(getProvider(), visitorContext); + JsonSerializer ser = getSer(prop); + if (ser != null) { + JavaType type = prop.getType(); + if (type == null) { + throw new IllegalStateException("Missing type for property '"+prop.getName()+"'"); + } + ser.acceptJsonFormatVisitor(visitor, type); + } + return visitor.finalSchema(); + } + + protected JsonSchema propertySchema(JsonFormatVisitable handler, JavaType propertyTypeHint) + throws JsonMappingException + { + // check if we've seen this argument's sub-schema already and return a reference-schema if we have + if (visitorContext != null) { + String seenSchemaUri = visitorContext.getSeenSchemaUri(propertyTypeHint); + if (seenSchemaUri != null) { + return new ReferenceSchema(seenSchemaUri); + } + } + + SchemaFactoryWrapper visitor = wrapperFactory.getWrapper(getProvider(), visitorContext); + handler.acceptJsonFormatVisitor(visitor, propertyTypeHint); + return visitor.finalSchema(); + } + + protected JsonSerializer getSer(BeanProperty prop) + throws JsonMappingException + { + JsonSerializer ser = null; + // 26-Jul-2013, tatu: This is ugly, should NOT require cast... + if (prop instanceof BeanPropertyWriter) { + ser = ((BeanPropertyWriter)prop).getSerializer(); + } + if (ser == null) { + ser = getProvider().findValueSerializer(prop.getType(), prop); + } + return ser; + } + + @Override + public Visitor setVisitorContext(VisitorContext rvc) { + visitorContext = rvc; + return this; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ObjectVisitorDecorator.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ObjectVisitorDecorator.java new file mode 100644 index 00000000..f3152152 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/ObjectVisitorDecorator.java @@ -0,0 +1,60 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * @author cponomaryov + */ +public class ObjectVisitorDecorator implements JsonObjectFormatVisitor, JsonSchemaProducer { + + protected ObjectVisitor objectVisitor; + + public ObjectVisitorDecorator(ObjectVisitor objectVisitor) { + this.objectVisitor = objectVisitor; + } + + @Override + public JsonSchema getSchema() { + return objectVisitor.getSchema(); + } + + @Override + public SerializerProvider getProvider() { + return objectVisitor.getProvider(); + } + + @Override + @Deprecated // since 2.5 + public void setProvider(SerializerProvider serializerProvider) { + if (objectVisitor.getProvider() == null) { + objectVisitor.setProvider(serializerProvider); + } + } + + @Override + public void optionalProperty(BeanProperty writer) throws JsonMappingException { + objectVisitor.optionalProperty(writer); + } + + @Override + public void optionalProperty(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + objectVisitor.optionalProperty(name, handler, propertyTypeHint); + } + + @Override + public void property(BeanProperty writer) throws JsonMappingException { + objectVisitor.property(writer); + } + + @Override + public void property(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + objectVisitor.property(name, handler, propertyTypeHint); + } + +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/SchemaFactoryWrapper.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/SchemaFactoryWrapper.java new file mode 100644 index 00000000..c1beb661 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/SchemaFactoryWrapper.java @@ -0,0 +1,168 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonAnyFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonBooleanFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNullFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.AnySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.BooleanSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.IntegerSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NullSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; + +/** + * @author jphelan + * @author tsaloranta + */ +public class SchemaFactoryWrapper implements JsonFormatVisitorWrapper, Visitor +{ + protected FormatVisitorFactory visitorFactory; + protected JsonSchemaFactory schemaProvider; + protected SerializerProvider provider; + protected JsonSchema schema; + protected VisitorContext visitorContext; + + public SchemaFactoryWrapper() { + this(null, new WrapperFactory()); + } + + public SchemaFactoryWrapper(SerializerProvider p) { + this(p, new WrapperFactory()); + } + + protected SchemaFactoryWrapper(WrapperFactory wrapperFactory) { + this(null, wrapperFactory); + } + + protected SchemaFactoryWrapper(SerializerProvider p, WrapperFactory wrapperFactory) { + provider = p; + schemaProvider = new JsonSchemaFactory(); + visitorFactory = new FormatVisitorFactory(wrapperFactory); + } + + /* + /********************************************************************* + /* JsonFormatVisitorWrapper implementation + /********************************************************************* + */ + + @Override + public SerializerProvider getProvider() { + return provider; + } + + @Override + public void setProvider(SerializerProvider p) { + provider = p; + } + + @Override + public JsonAnyFormatVisitor expectAnyFormat(JavaType convertedType) { + AnySchema s = schemaProvider.anySchema(); + this.schema = s; + return visitorFactory.anyFormatVisitor(s); + } + + @Override + public JsonArrayFormatVisitor expectArrayFormat(JavaType convertedType) { + ArraySchema s = schemaProvider.arraySchema(); + this.schema = s; + return visitorFactory.arrayFormatVisitor(provider, s, visitorContext); + } + + @Override + public JsonBooleanFormatVisitor expectBooleanFormat(JavaType convertedType) { + BooleanSchema s = schemaProvider.booleanSchema(); + this.schema = s; + return visitorFactory.booleanFormatVisitor(s); + } + + @Override + public JsonIntegerFormatVisitor expectIntegerFormat(JavaType convertedType) { + IntegerSchema s = schemaProvider.integerSchema(); + this.schema = s; + return visitorFactory.integerFormatVisitor(s); + } + + @Override + public JsonNullFormatVisitor expectNullFormat(JavaType convertedType) { + NullSchema s = schemaProvider.nullSchema(); + schema = s; + return visitorFactory.nullFormatVisitor(s); + } + + @Override + public JsonNumberFormatVisitor expectNumberFormat(JavaType convertedType) { + NumberSchema s = schemaProvider.numberSchema(); + schema = s; + return visitorFactory.numberFormatVisitor(s); + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { + ObjectSchema s = schemaProvider.objectSchema(); + schema = s; + + // if we don't already have a recursive visitor context, create one + if (visitorContext == null) { + visitorContext = new VisitorContext(); + } + + // give each object schema a reference id and keep track of the ones we've seen + String schemaUri = visitorContext.addSeenSchemaUri(convertedType); + if (schemaUri != null) { + s.setId(schemaUri); + } + + return visitorFactory.objectFormatVisitor(provider, s, visitorContext); + } + + @Override + public JsonStringFormatVisitor expectStringFormat(JavaType convertedType) { + StringSchema s = schemaProvider.stringSchema(); + schema = s; + return visitorFactory.stringFormatVisitor(s); + } + + @Override + public JsonMapFormatVisitor expectMapFormat(JavaType type) + throws JsonMappingException + { + /* 22-Nov-2012, tatu: Looks as if JSON Schema did not have + * concept of Map (distinct from Record or Object); so best + * we can do is to consider it a vague kind-a Object... + */ + ObjectSchema s = schemaProvider.objectSchema(); + schema = s; + return visitorFactory.mapFormatVisitor(provider, s, visitorContext); + } + + @Override + public SchemaFactoryWrapper setVisitorContext(VisitorContext rvc) { + visitorContext = rvc; + return this; + } + + /* + /********************************************************************* + /* API + /********************************************************************* + */ + + public JsonSchema finalSchema() { + return schema; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/StringVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/StringVisitor.java new file mode 100644 index 00000000..f17116e7 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/StringVisitor.java @@ -0,0 +1,38 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import java.util.Set; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; + +public class StringVisitor extends JsonStringFormatVisitor.Base + implements JsonSchemaProducer +{ + protected final StringSchema schema; + + public StringVisitor(StringSchema schema) { + this.schema = schema; + } + + /* + /********************************************************************* + /* JsonSchemaProducer + /********************************************************************* + */ + + @Override + public void enumTypes(Set enums) { + schema.setEnums(enums); + } + + @Override + public void format(JsonValueFormat format) { + schema.setFormat(format); + } + + @Override + public StringSchema getSchema() { + return schema; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/StructuredTypeVisitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/StructuredTypeVisitor.java new file mode 100644 index 00000000..99d72875 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/StructuredTypeVisitor.java @@ -0,0 +1,25 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.SerializerProvider; + +public abstract class StructuredTypeVisitor implements JsonSchemaProducer +{ + protected SerializerProvider provider; + + protected StructuredTypeVisitor(SerializerProvider provider) + { + this.provider = provider; + } + + // // // Partial implementation for visitors; handling of SerializerProvider + + public SerializerProvider getProvider() { + return provider; + } + + public void setProvider(SerializerProvider p) { + provider = p; + } + +} + diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/Visitor.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/Visitor.java new file mode 100644 index 00000000..1c678c1f --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/Visitor.java @@ -0,0 +1,8 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +/** + * Created by adb on 6/12/14. + */ +public interface Visitor { + Visitor setVisitorContext(VisitorContext rvc); +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/VisitorContext.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/VisitorContext.java new file mode 100644 index 00000000..431d03b3 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/VisitorContext.java @@ -0,0 +1,26 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.JavaType; + +import java.util.HashSet; + +public class VisitorContext +{ + private final HashSet seenSchemas = new HashSet(); + + public String addSeenSchemaUri(JavaType aSeenSchema) { + if (aSeenSchema != null && !aSeenSchema.isPrimitive()) { + seenSchemas.add(aSeenSchema); + return javaTypeToUrn(aSeenSchema); + } + return null; + } + + public String getSeenSchemaUri(JavaType aSeenSchema) { + return (seenSchemas.contains(aSeenSchema)) ? javaTypeToUrn(aSeenSchema) : null; + } + + public String javaTypeToUrn(JavaType jt) { + return "urn:jsonschema:" + jt.toCanonical().replace('.', ':').replace('$', ':'); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/WrapperFactory.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/WrapperFactory.java new file mode 100644 index 00000000..ee216b2e --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/factories/WrapperFactory.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * Exists to supply {@link SchemaFactoryWrapper} or its subclasses + * to nested schema factories. + * @author jphelan + */ +public class WrapperFactory +{ + public SchemaFactoryWrapper getWrapper(SerializerProvider provider) { + return new SchemaFactoryWrapper(provider); + } + + public SchemaFactoryWrapper getWrapper(SerializerProvider provider, VisitorContext rvc) { + SchemaFactoryWrapper wrapper = new SchemaFactoryWrapper(provider); + wrapper.setVisitorContext(rvc); + return wrapper; + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AdditionalItemsDeserializer.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AdditionalItemsDeserializer.java new file mode 100644 index 00000000..86fc5db7 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AdditionalItemsDeserializer.java @@ -0,0 +1,37 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonTokenId; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * @author Yoann Rodière (adapted from {@link AdditionalPropertiesDeserializer}, by Ignacio del Valle Alles) + */ +public class AdditionalItemsDeserializer extends JsonDeserializer +{ + @Override + public ArraySchema.AdditionalItems deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + if (p.hasCurrentToken()) { + switch (p.getCurrentTokenId()) { + case JsonTokenId.ID_TRUE: + return null; // "additionalItems":true is the default + case JsonTokenId.ID_FALSE: + return new ArraySchema.NoAdditionalItems(); + case JsonTokenId.ID_START_OBJECT: + case JsonTokenId.ID_FIELD_NAME: + case JsonTokenId.ID_END_OBJECT: + // 29-Dec-2015, tatu: may need/want to use property value reader in future but for now: + JsonSchema innerSchema = ctxt.readValue(p, JsonSchema.class); + return new ArraySchema.SchemaAdditionalItems(innerSchema); + } + } + return ctxt.reportInputMismatch(this, +"additionalItems nodes can only be of type boolean or object, got token of type: %s", p.getCurrentToken()); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AdditionalPropertiesDeserializer.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AdditionalPropertiesDeserializer.java new file mode 100644 index 00000000..3afddba8 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AdditionalPropertiesDeserializer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013 FasterXML. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * @author Ignacio del Valle Alles + */ +public class AdditionalPropertiesDeserializer + extends JsonDeserializer +{ + @Override + public ObjectSchema.AdditionalProperties deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + if (p.hasCurrentToken()) { + switch (p.getCurrentTokenId()) { + case JsonTokenId.ID_TRUE: + return null; // "additionalProperties":true is the default + case JsonTokenId.ID_FALSE: + return ObjectSchema.NoAdditionalProperties.instance; + case JsonTokenId.ID_START_OBJECT: + case JsonTokenId.ID_FIELD_NAME: + case JsonTokenId.ID_END_OBJECT: + // 29-Dec-2015, tatu: may need/want to use property value reader in future but for now: + JsonSchema innerSchema = ctxt.readValue(p, JsonSchema.class); + return new ObjectSchema.SchemaAdditionalProperties(innerSchema); + } + } + return ctxt.reportInputMismatch(this, +"additionalProperties nodes can only be of type boolean or object, got token of type: %s", p.getCurrentToken()); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AnySchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AnySchema.java new file mode 100644 index 00000000..2a81bfee --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/AnySchema.java @@ -0,0 +1,32 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class represents a {@link JsonSchema} of type any + * @author jphelan + */ +public class AnySchema extends SimpleTypeSchema +{ + public AnySchema() { } + + @Override + public AnySchema asAnySchema() { return this; } + + @Override + public boolean isAnySchema() { return true; } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.ANY; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof AnySchema)) return false; + return _equals((AnySchema) obj); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ArraySchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ArraySchema.java new file mode 100644 index 00000000..4861f11f --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ArraySchema.java @@ -0,0 +1,295 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import java.io.IOException; + +import com.fasterxml.jackson.annotation.*; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/* + * This attribute defines the allowed items in an instance array, and + MUST be a jsonSchema or an array of jsonSchemas. The default value is an + empty jsonSchema which allows any value for items in the instance array. + */ +public class ArraySchema extends ContainerTypeSchema +{ + /** + * see {@link AdditionalItems} + */ + @JsonProperty + protected ArraySchema.AdditionalItems additionalItems; + + /** + * see {@link Items} + */ + @JsonProperty + @JsonDeserialize(using = ItemsDeserializer.class) + protected ArraySchema.Items items; + + /**This attribute defines the maximum number of values in an array*/ + @JsonProperty + protected Integer maxItems; + + /**This attribute defines the minimum number of values in an array*/ + @JsonProperty + protected Integer minItems; + + /** + * This attribute indicates that all items in an array instance MUST be + unique (contains no two identical values). + + Two instance are consider equal if they are both of the same type + and: + + are null; or are booleans/numbers/strings and have the same value; or + + are arrays, contains the same number of items, and each item in + the array is equal to the corresponding item in the other array; + or + + are objects, contains the same property names, and each property + in the object is equal to the corresponding property in the other + object. + */ + @JsonProperty + protected Boolean uniqueItems; + + @Override + public ArraySchema asArraySchema() { return this; } + + public ArraySchema.AdditionalItems getAdditionalItems() { + return additionalItems; + } + + public ArraySchema.Items getItems() { + return items; + } + + public Integer getMaxItems() { + return maxItems; + } + + public Integer getMinItems() { + return minItems; + } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.ARRAY; + } + + public Boolean getUniqueItems() { + return uniqueItems; + } + + @Override + public boolean isArraySchema() { return true; } + + public void setAdditionalItems(ArraySchema.AdditionalItems additionalItems) { + this.additionalItems = additionalItems; + } + + public void setItems(ArraySchema.Items items) { + this.items = items; + } + + public void setItemsSchema(JsonSchema jsonSchema) { + items = new SingleItems(jsonSchema); + } + + public void setMaxItems(Integer maxItems) { + this.maxItems = maxItems; + } + + public void setMinItems(Integer minItems) { + this.minItems = minItems; + } + + public void setUniqueItems(Boolean uniqueItems) { + this.uniqueItems = uniqueItems; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof ArraySchema)) return false; + return _equals((ArraySchema) obj); + } + + protected boolean _equals(ArraySchema that) + { + return equals(getAdditionalItems(), that.getAdditionalItems()) + && equals(getItems(), that.getItems()) + && equals(getMaxItems(), that.getMaxItems()) + && equals(getMinItems(), that.getMinItems()) + && equals(getUniqueItems(), that.getUniqueItems()) + && super._equals(that); + } + + /** + * This provides a definition for additional items in an array instance + * when tuple definitions of the items is provided. + */ + @JsonDeserialize(using = AdditionalItemsDeserializer.class) + public static abstract class AdditionalItems { + // simple marker class, deserializer and concrete impl have all functionality + } + + /** + * When this attribute value is an array of jsonSchemas and the instance + value is an array, each position in the instance array MUST conform + to the jsonSchema in the corresponding position for this array. This + called tuple typing. When tuple typing is used, additional items are + allowed, disallowed, or constrained by the "additionalItems" + */ + public static class ArrayItems extends ArraySchema.Items { + @JsonProperty + private JsonSchema[] jsonSchemas; + + public ArrayItems(JsonSchema[] jsonSchemas) { + this.jsonSchemas = jsonSchemas; + } + + @Override + public ArrayItems asArrayItems() { return this; } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Items) { + ArrayItems that = (ArrayItems) obj; + return JsonSchema.equals(getJsonSchemas(), that.getJsonSchemas()); + } else { + return false; + } + } + + public JsonSchema[] getJsonSchemas() { + return jsonSchemas; + } + + @Override + public boolean isArrayItems() { return true; } + } + + public static class ItemsDeserializer extends StdDeserializer + { + private static final long serialVersionUID = 1L; + + public ItemsDeserializer() { + super(Items.class); + } + + @Override + public Items deserialize(JsonParser parser, + DeserializationContext context) throws IOException + { + // 07-Oct-2015, tatu: Could further optimize by fetching delegating + // deserializer in `createContextual`, but should do for now + if (parser.isExpectedStartArrayToken()) { + JsonSchema[] schemas = context.readValue(parser, JsonSchema[].class); + return new ArrayItems(schemas); + } + JsonSchema schema = context.readValue(parser, JsonSchema.class); + return new SingleItems(schema); + } + } + + /** + * This attribute defines the allowed items in an instance array, and + MUST be a jsonSchema or an array of jsonSchemas. The default value is an + empty jsonSchema which allows any value for items in the instance array. + */ + public static abstract class Items { + + @JsonIgnore + public boolean isSingleItems() { return false; } + + @JsonIgnore + public boolean isArrayItems() { return false; } + + public SingleItems asSingleItems() { return null; } + public ArrayItems asArrayItems() { return null; } + } + + /** + * This can be false + to indicate additional items in the array are not allowed + */ + public static class NoAdditionalItems extends AdditionalItems { + @Override + public boolean equals(Object obj) { + return obj instanceof NoAdditionalItems; + } + @JsonValue + public Boolean value() { return false; } + } + + /** + * or it can + be a jsonSchema that defines the jsonSchema of the additional items. + */ + public static class SchemaAdditionalItems extends AdditionalItems { + + @JsonIgnore + private JsonSchema jsonSchema; + + public SchemaAdditionalItems(JsonSchema schema) { + jsonSchema = schema; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof SchemaAdditionalItems && + JsonSchema.equals(getJsonSchema(), ((SchemaAdditionalItems)obj).getJsonSchema()); + } + + @JsonValue + public JsonSchema getJsonSchema() { + return jsonSchema; + } + } + + /** + * When this attribute value is a jsonSchema and the instance value is an + array, then all the items in the array MUST be valid according to the + jsonSchema. + */ + public static class SingleItems extends ArraySchema.Items { + @JsonIgnore + private JsonSchema jsonSchema; + + public SingleItems(JsonSchema jsonSchema) { + this.jsonSchema = jsonSchema; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof SingleItems && + JsonSchema.equals(getSchema(), ((SingleItems)obj).getSchema()); + } + + @JsonValue + public JsonSchema getSchema() { + return jsonSchema; + } + + public void setSchema(JsonSchema jsonSchema) { + this.jsonSchema = jsonSchema; + } + + @Override + public boolean isSingleItems() { return true; } + + @Override + public SingleItems asSingleItems() { return this; } + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/BooleanSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/BooleanSchema.java new file mode 100644 index 00000000..3f269edc --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/BooleanSchema.java @@ -0,0 +1,37 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class represents a {@link JsonSchema} of type boolean + * @author jphelan + * + */ +public class BooleanSchema extends ValueTypeSchema +{ + @Override + public boolean isBooleanSchema() { return true; } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.BOOLEAN; + } + + @Override + public BooleanSchema asBooleanSchema() { return this; } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof BooleanSchema)) return false; + return _equals((BooleanSchema) obj); + } + + protected boolean _equals(BooleanSchema that) + { + return super._equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ContainerTypeSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ContainerTypeSchema.java new file mode 100644 index 00000000..14382ffa --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ContainerTypeSchema.java @@ -0,0 +1,88 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import java.util.Collections; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class encapsulates the functionality of container type {@link JsonSchema} + * Array and Object + * @author jphelan + */ +public abstract class ContainerTypeSchema extends SimpleTypeSchema +{ + /** + * This provides an enumeration of all possible values that are valid + for the instance property. This MUST be an array, and each item in + the array represents a possible value for the instance value. If + this attribute is defined, the instance value MUST be one of the + values in the array in order for the schema to be valid. Comparison + of enum values uses the same algorithm as defined in "uniqueItems" + (Section 5.15). + */ + @JsonProperty(value = "enum", required = true) + protected Set enums = Collections.emptySet(); + + /** + * This provides an enumeration of all possible values that are valid + for the instance property. This MUST be an array, and each item in + the array represents a possible value for the instance value. If + this attribute is defined, the instance value MUST be one of the + values in the array in order for the schema to be valid. Comparison + of enum values uses the same algorithm as defined in "uniqueItems" + (Section 5.15). + */ + @JsonProperty(value = "oneOf", required = true) + protected Set oneOf = Collections.emptySet(); + + /** + * @deprecated Since 2.7 + */ + @Deprecated + @Override + public ContainerTypeSchema asContainerSchema() { + return asContainerTypeSchema(); + } + + /** + * @since 2.7 + */ + @Override + public ContainerTypeSchema asContainerTypeSchema() { return this; } + + public Set getEnums() { + return enums; + } + + @Override + public boolean isContainerTypeSchema() { return true; } + + public void setEnums(Set enums) { + this.enums = enums; + } + + public Set getOneOf() { + return oneOf; + } + + public void setOneOf(Set oneOf) { + this.oneOf = oneOf; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof ContainerTypeSchema)) return false; + return _equals((ContainerTypeSchema) obj); + } + + protected boolean _equals(ContainerTypeSchema that) + { + return equals(getOneOf(), that.getOneOf()) + && super._equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/HyperSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/HyperSchema.java new file mode 100644 index 00000000..619e3b2d --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/HyperSchema.java @@ -0,0 +1,393 @@ + package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +import java.lang.annotation.Annotation; + + /** + * This class represents the HyperSchema portion of a {@link JsonSchema} + * It is a skeleton intended as a starting point for customization. + * @author jphelan + */ +public class HyperSchema extends JsonSchema +{ + /** + * This attribute indicates that the instance property SHOULD NOT be + changed. Attempts by a user agent to modify the value of this + property are expected to be rejected by a server. + */ + @JsonProperty + protected String readOnly; + + /** + * If the instance property value is a string, this attribute defines + that the string SHOULD be interpreted as binary data and decoded + using the encoding named by this jsonSchema property. RFC 2045, Sec 6.1 + [RFC2045] lists the possible values for this property. + */ + @JsonProperty + protected String contentEncoding; + + /** + * This attribute is a URI that defines what the instance's URI MUST + start with in order to validate. The value of the "pathStart" + attribute MUST be resolved as per RFC 3986, Sec 5 [RFC3986], and is + relative to the instance's URI. + + When multiple schemas have been referenced for an instance, the user + agent can determine if this jsonSchema is applicable for a particular + instance by determining if the URI of the instance begins with the + the value of the "pathStart" attribute. If the URI of the instance + does not start with this URI, or if another jsonSchema specifies a + starting URI that is longer and also matches the instance, this + jsonSchema SHOULD NOT be applied to the instance. Any jsonSchema that does + not have a pathStart attribute SHOULD be considered applicable to all + the instances for which it is referenced. + */ + @JsonProperty + protected String pathStart; + + /** + * This attribute defines the media type of the instance representations + that this jsonSchema is defining. + */ + @JsonProperty + protected String mediaType; + + /** + * This property indicates the fragment resolution protocol to use for + resolving fragment identifiers in URIs within the instance + representations. This applies to the instance object URIs and all + children of the instance object's URIs. The default fragment + resolution protocol is "slash-delimited", which is defined below. + Other fragment resolution protocols MAY be used, but are not defined + in this document. + + The fragment identifier is based on RFC 2396, Sec 5 [RFC2396], and + defines the mechanism for resolving references to entities within a + document. + */ + @JsonProperty + protected String fragmentResolution; + /** + * 6.2.1. slash-delimited fragment resolution + + With the slash-delimited fragment resolution protocol, the fragment + identifier is interpreted as a series of property reference tokens + that start with and are delimited by the "/" character (\x2F). Each + property reference token is a series of unreserved or escaped URI + characters. Each property reference token SHOULD be interpreted, + starting from the beginning of the fragment identifier, as a path + reference in the target JSON structure. The final target value of + the fragment can be determined by starting with the root of the JSON + structure from the representation of the resource identified by the + pre-fragment URI. If the target is a JSON object, then the new + target is the value of the property with the name identified by the + next property reference token in the fragment. If the target is a + JSON array, then the target is determined by finding the item in + array the array with the index defined by the next property reference + token (which MUST be a number). The target is successively updated + for each property reference token, until the entire fragment has been + traversed. + + Property names SHOULD be URI-encoded. In particular, any "/" in a + property name MUST be encoded to avoid being interpreted as a + property delimiter. + + For example, for the following JSON representation: + + { + "foo":{ + "anArray":[ + {"prop":44} + ], + "another prop":{ + "baz":"A string" + } + } + } + + The following fragment identifiers would be resolved: + + fragment identifier resolution + ------------------- ---------- + # self, the root of the resource itself + #/foo the object referred to by the foo property + #/foo/another%20prop the object referred to by the "another prop" + property of the object referred to by the + "foo" property + #/foo/another%20prop/baz the string referred to by the value of "baz" + property of the "another prop" property of + the object referred to by the "foo" property + #/foo/anArray/0 the first object in the "anArray" array + + 6.2.2. dot-delimited fragment resolution + + The dot-delimited fragment resolution protocol is the same as slash- + delimited fragment resolution protocol except that the "." character + (\x2E) is used as the delimiter between property names (instead of + "/") and the path does not need to start with a ".". For example, + #.foo and #foo are a valid fragment identifiers for referencing the + value of the foo propery. + */ + + @JsonProperty + protected LinkDescriptionObject[] links; + + @Override + public JsonFormatTypes getType() { + return null; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof HyperSchema)) return false; + return _equals((HyperSchema) obj); + } + + protected boolean _equals(HyperSchema that) + { + return equals(readOnly, that.readOnly) + && equals(contentEncoding, that.contentEncoding) + && equals(pathStart, that.pathStart) + && equals(mediaType, that.mediaType) + && equals(fragmentResolution, that.fragmentResolution) + && arraysEqual(links, that.links) + && super._equals(that); + } + + /** + * A link description object is used to describe link relations. In the + context of a jsonSchema, it defines the link relations of the instances + of the jsonSchema, and can be parameterized by the instance values. The + link description format can be used on its own in regular (non-jsonSchema + documents), and use of this format can be declared by referencing the + normative link description jsonSchema as the the jsonSchema for the data + structure that uses the links. + */ + public static class LinkDescriptionObject + { + /** + * The value of the "href" link description property indicates the + target URI of the related resource. The value of the instance + property SHOULD be resolved as a URI-Reference per RFC 3986 [RFC3986] + and MAY be a relative URI. The base URI to be used for relative + resolution SHOULD be the URI used to retrieve the instance object + (not the jsonSchema) when used within a jsonSchema. Also, when links are + used within a jsonSchema, the URI SHOULD be parametrized by the property + values of the instance object, if property values exist for the + corresponding variables in the template (otherwise they MAY be + provided from alternate sources, like user input). + + Instance property values SHOULD be substituted into the URIs where + matching braces ('{', '}') are found surrounding zero or more + characters, creating an expanded URI. Instance property value + substitutions are resolved by using the text between the braces to + denote the property name from the instance to get the value to + substitute. For example, if an href value is defined: + + http://somesite.com/{id} + + Then it would be resolved by replace the value of the "id" property + value from the instance object. If the value of the "id" property + was "45", the expanded URI would be: + + http://somesite.com/45 + + If matching braces are found with the string "@" (no quotes) between + the braces, then the actual instance value SHOULD be used to replace + the braces, rather than a property value. This should only be used + in situations where the instance is a scalar (string, boolean, or + number), and not for objects or arrays. + + */ + @JsonProperty + protected String href; + + /** + * The value of the "rel" property indicates the name of the relation to + the target resource. The relation to the target SHOULD be + interpreted as specifically from the instance object that the jsonSchema + (or sub-jsonSchema) applies to, not just the top level resource that + contains the object within its hierarchy. If a resource JSON + representation contains a sub object with a property interpreted as a + link, that sub-object holds the relation with the target. A relation + to target from the top level resource MUST be indicated with the + jsonSchema describing the top level JSON representation. + + Relationship definitions SHOULD NOT be media type dependent, and + users are encouraged to utilize existing accepted relation + definitions, including those in existing relation registries (see RFC + 4287 [RFC4287]). However, we define these relations here for clarity + of normative interpretation within the context of JSON hyper jsonSchema + defined relations: + + self If the relation value is "self", when this property is + encountered in the instance object, the object represents a + resource and the instance object is treated as a full + representation of the target resource identified by the specified + URI. + + full This indicates that the target of the link is the full + representation for the instance object. The object that contains + this link possibly may not be the full representation. + + describedby This indicates the target of the link is the jsonSchema for + the instance object. This MAY be used to specifically denote the + schemas of objects within a JSON object hierarchy, facilitating + polymorphic type data structures. + + root This relation indicates that the target of the link SHOULD be + treated as the root or the body of the representation for the + purposes of user agent interaction or fragment resolution. All + other properties of the instance objects can be regarded as meta- + data descriptions for the data. + + The following relations are applicable for schemas (the jsonSchema as the + "from" resource in the relation): + + instances This indicates the target resource that represents + collection of instances of a jsonSchema. + + create This indicates a target to use for creating new instances of + a jsonSchema. This link definition SHOULD be a submission link with a + non-safe method (like POST). + + For example, if a jsonSchema is defined: + + { + "links": [ + { + "rel": "self" + "href": "{id}" + }, + { + "rel": "up" + "href": "{upId}" + }, + { + "rel": "children" + "href": "?upId={id}" + } + ] + } + + And if a collection of instance resource's JSON representation was + retrieved: + + GET /Resource/ + + [ + { + "id": "thing", + "upId": "parent" + }, + { + "id": "thing2", + "upId": "parent" + } + ] + + This would indicate that for the first item in the collection, its + own (self) URI would resolve to "/Resource/thing" and the first + item's "up" relation SHOULD be resolved to the resource at + "/Resource/parent". The "children" collection would be located at + "/Resource/?upId=thing". + */ + @JsonProperty + protected String rel; + + /** + * This property value is a jsonSchema that defines the expected structure + of the JSON representation of the target of the link. + */ + @JsonProperty + protected JsonSchema targetSchema; + + /** + * This attribute defines which method can be used to access the target + resource. In an HTTP environment, this would be "GET" or "POST" + (other HTTP methods such as "PUT" and "DELETE" have semantics that + are clearly implied by accessed resources, and do not need to be + defined here). This defaults to "GET". + */ + @JsonProperty + protected String method; + + /** + * If present, this property indicates a query media type format that + the server supports for querying or posting to the collection of + instances at the target resource. The query can be suffixed to the + target URI to query the collection with property-based constraints on + the resources that SHOULD be returned from the server or used to post + data to the resource (depending on the method). For example, with + the following jsonSchema: + + { + "links":[ + { + "enctype":"application/x-www-form-urlencoded", + "method":"GET", + "href":"/Product/", + "properties":{ + "name":{"description":"name of the product"} + } + } + ] + } + This indicates that the client can query the server for instances + that have a specific name: + + /Product/?name=Slinky + + If no enctype or method is specified, only the single URI specified + by the href property is defined. If the method is POST, + "application/json" is the default media type. + */ + @JsonProperty + protected String enctype; + + /** + * This attribute contains a jsonSchema which defines the acceptable + structure of the submitted request (for a GET request, this jsonSchema + would define the properties for the query string and for a POST + request, this would define the body). + */ + @JsonProperty + protected JsonSchema jsonSchema; + + public LinkDescriptionObject(Annotation link) { } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof HyperSchema)) return false; + return _equals((LinkDescriptionObject) obj); + } + + protected boolean _equals(LinkDescriptionObject that) + { + return equals(href, that.href) + && equals(rel, that.rel) + && equals(targetSchema, that.targetSchema) + && equals(method, that.method) + && equals(enctype, that.enctype) + && equals(jsonSchema, that.jsonSchema) + ; + } + + protected static boolean equals(Object object1, Object object2) { + if (object1 == null) { + return object2 == null; + } + return object1.equals(object2); + } + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/IntegerSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/IntegerSchema.java new file mode 100644 index 00000000..97893516 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/IntegerSchema.java @@ -0,0 +1,55 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class represents a {@link JsonSchema} as an integer type + * @author jphelan + * + */ +public class IntegerSchema extends NumberSchema +{ + /** + * This attribute defines what value the number instance must be + divisible by with no remainder (the result of the division must be an + integer.) The value of this attribute SHOULD NOT be 0. + */ + private Integer divisibleBy; + + @Override + public boolean isIntegerSchema() { return true; } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.INTEGER; + } + + @Override + public IntegerSchema asIntegerSchema() { return this; } + + @JsonProperty + public Integer getDivisibleBy() { + return divisibleBy; + } + + public void setDivisibleBy(Integer divisibleBy) { + this.divisibleBy = divisibleBy; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof IntegerSchema)) return false; + return _equals((IntegerSchema) obj); + } + + protected boolean _equals(IntegerSchema that) + { + return equals(getDivisibleBy(), that.getDivisibleBy()) + && super.equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/LinkDescriptionObject.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/LinkDescriptionObject.java new file mode 100644 index 00000000..3cdf51f9 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/LinkDescriptionObject.java @@ -0,0 +1,300 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * A link description object is used to describe link relations. In the + context of a jsonSchema, it defines the link relations of the instances + of the jsonSchema, and can be parameterized by the instance values. The + link description format can be used on its own in regular (non-jsonSchema + documents), and use of this format can be declared by referencing the + normative link description jsonSchema as the the jsonSchema for the data + structure that uses the links. + */ +@JsonInclude(value = Include.NON_EMPTY) +public class LinkDescriptionObject { + + /** + * The value of the "href" link description property indicates the + target URI of the related resource. The value of the instance + property SHOULD be resolved as a URI-Reference per RFC 3986 [RFC3986] + and MAY be a relative URI. The base URI to be used for relative + resolution SHOULD be the URI used to retrieve the instance object + (not the jsonSchema) when used within a jsonSchema. Also, when links are + used within a jsonSchema, the URI SHOULD be parametrized by the property + values of the instance object, if property values exist for the + corresponding variables in the template (otherwise they MAY be + provided from alternate sources, like user input). + + Instance property values SHOULD be substituted into the URIs where + matching braces ('{', '}') are found surrounding zero or more + characters, creating an expanded URI. Instance property value + substitutions are resolved by using the text between the braces to + denote the property name from the instance to get the value to + substitute. For example, if an href value is defined: + + http://somesite.com/{id} + + Then it would be resolved by replace the value of the "id" property + value from the instance object. If the value of the "id" property + was "45", the expanded URI would be: + + http://somesite.com/45 + + If matching braces are found with the string "@" (no quotes) between + the braces, then the actual instance value SHOULD be used to replace + the braces, rather than a property value. This should only be used + in situations where the instance is a scalar (string, boolean, or + number), and not for objects or arrays. + + */ + @JsonProperty + private String href; + + /** + * The value of the "rel" property indicates the name of the relation to + the target resource. The relation to the target SHOULD be + interpreted as specifically from the instance object that the jsonSchema + (or sub-jsonSchema) applies to, not just the top level resource that + contains the object within its hierarchy. If a resource JSON + representation contains a sub object with a property interpreted as a + link, that sub-object holds the relation with the target. A relation + to target from the top level resource MUST be indicated with the + jsonSchema describing the top level JSON representation. + + Relationship definitions SHOULD NOT be media type dependent, and + users are encouraged to utilize existing accepted relation + definitions, including those in existing relation registries (see RFC + 4287 [RFC4287]). However, we define these relations here for clarity + of normative interpretation within the context of JSON hyper jsonSchema + defined relations: + + self If the relation value is "self", when this property is + encountered in the instance object, the object represents a + resource and the instance object is treated as a full + representation of the target resource identified by the specified + URI. + + full This indicates that the target of the link is the full + representation for the instance object. The object that contains + this link possibly may not be the full representation. + + describedby This indicates the target of the link is the jsonSchema for + the instance object. This MAY be used to specifically denote the + schemas of objects within a JSON object hierarchy, facilitating + polymorphic type data structures. + + root This relation indicates that the target of the link SHOULD be + treated as the root or the body of the representation for the + purposes of user agent interaction or fragment resolution. All + other properties of the instance objects can be regarded as meta- + data descriptions for the data. + + The following relations are applicable for schemas (the jsonSchema as the + "from" resource in the relation): + + instances This indicates the target resource that represents + collection of instances of a jsonSchema. + + create This indicates a target to use for creating new instances of + a jsonSchema. This link definition SHOULD be a submission link with a + non-safe method (like POST). + + For example, if a jsonSchema is defined: + + { + "links": [ + { + "rel": "self" + "href": "{id}" + }, + { + "rel": "up" + "href": "{upId}" + }, + { + "rel": "children" + "href": "?upId={id}" + } + ] + } + + And if a collection of instance resource's JSON representation was + retrieved: + + GET /Resource/ + + [ + { + "id": "thing", + "upId": "parent" + }, + { + "id": "thing2", + "upId": "parent" + } + ] + + This would indicate that for the first item in the collection, its + own (self) URI would resolve to "/Resource/thing" and the first + item's "up" relation SHOULD be resolved to the resource at + "/Resource/parent". The "children" collection would be located at + "/Resource/?upId=thing". + */ + @JsonProperty + private String rel; + + /** + * This property value is a jsonSchema that defines the expected structure + of the JSON representation of the target of the link. + */ + @JsonProperty + private JsonSchema targetSchema; + + /** + * This attribute defines which method can be used to access the target + resource. In an HTTP environment, this would be "GET" or "POST" + (other HTTP methods such as "PUT" and "DELETE" have semantics that + are clearly implied by accessed resources, and do not need to be + defined here). This defaults to "GET". + */ + @JsonProperty + private String method; + + /** + * If present, this property indicates a query media type format that + the server supports for querying or posting to the collection of + instances at the target resource. The query can be suffixed to the + target URI to query the collection with property-based constraints on + the resources that SHOULD be returned from the server or used to post + data to the resource (depending on the method). For example, with + the following jsonSchema: + + { + "links":[ + { + "enctype":"application/x-www-form-urlencoded", + "method":"GET", + "href":"/Product/", + "properties":{ + "name":{"description":"name of the product"} + } + } + ] + } + This indicates that the client can query the server for instances + that have a specific name: + + /Product/?name=Slinky + + If no enctype or method is specified, only the single URI specified + by the href property is defined. If the method is POST, + "application/json" is the default media type. + */ + @JsonProperty + private String enctype; + + /** + * This attribute contains a jsonSchema which defines the acceptable + structure of the submitted request (for a GET request, this jsonSchema + would define the properties for the query string and for a POST + request, this would define the body). + */ + @JsonIgnore + private JsonSchema jsonSchema; + + /** + * This property defines a title for the link. The value must be a string. + User agents MAY use this title when presenting the link to the user. + */ + @JsonProperty + private String title; + + /** + * The value of this property is advisory only, and represents the media type RFC 2046 [RFC2046], + that is expected to be returned when fetching this resource. + */ + private String mediaType; + + public LinkDescriptionObject() {} + + public String getHref() { + return href; + } + + public LinkDescriptionObject setHref(String href) { + this.href = href; + return this; + } + + public String getRel() { + return rel; + } + + public LinkDescriptionObject setRel(String rel) { + this.rel = rel; + return this; + } + + public String getMethod() { + return method; + } + + public LinkDescriptionObject setMethod(String method) { + this.method = method; + return this; + } + + public String getEnctype() { + return enctype; + } + + public LinkDescriptionObject setEnctype(String enctype) { + this.enctype = enctype; + return this; + } + + public JsonSchema getTargetSchema() { + return targetSchema; + } + + public LinkDescriptionObject setTargetSchema(JsonSchema targetSchema) { + this.targetSchema = targetSchema; + return this; + } + + @JsonProperty + public JsonSchema getSchema() { + return jsonSchema; + } + + public LinkDescriptionObject setSchema(JsonSchema schema) { + this.jsonSchema = schema; + return this; + } + + public String getTitle() { + return title; + } + + public LinkDescriptionObject setTitle(String title) { + if(title != null && title.length() > 0) + this.title = title; + return this; + } + + public String getMediaType() { + return mediaType; + } + + public LinkDescriptionObject setMediaType(String mediaType) { + this.mediaType = mediaType; + return this; + } + +} + diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/NullSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/NullSchema.java new file mode 100644 index 00000000..b8878e1d --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/NullSchema.java @@ -0,0 +1,31 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class represents a {@link JsonSchema} as a null type + * @author jphelan + */ +public class NullSchema extends SimpleTypeSchema +{ + @Override + public NullSchema asNullSchema() { return this; } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.NULL; + } + + @Override + public boolean isNullSchema() { return true; } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof NullSchema)) return false; + return _equals((NullSchema) obj); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/NumberSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/NumberSchema.java new file mode 100644 index 00000000..cc1ce15b --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/NumberSchema.java @@ -0,0 +1,114 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class represents a {@link JsonSchema} as a number type + * @author jphelan + */ +public class NumberSchema extends ValueTypeSchema +{ + /** + * This attribute indicates if the value of the instance (if the + instance is a number) can not equal the number defined by the + "maximum" attribute. + */ + @JsonProperty + private Boolean exclusiveMaximum; + + /** + * This attribute indicates if the value of the instance (if the + instance is a number) can not equal the number defined by the + "minimum" attribute. + */ + @JsonProperty + private Boolean exclusiveMinimum; + + /**This attribute defines the maximum value of the instance property*/ + @JsonProperty + private Double maximum = null; + + /**This attribute defines the minimum value of the instance property*/ + @JsonProperty + private Double minimum = null; + + /** The value of the instance needs to be a multiple of this attribute */ + @JsonProperty + private Double multipleOf = null; + + @Override + public NumberSchema asNumberSchema() { return this; } + + public Boolean getExclusiveMaximum() { + return exclusiveMaximum; + } + + public Boolean getExclusiveMinimum() { + return exclusiveMinimum; + } + + public Double getMaximum() { + return maximum; + } + + public Double getMinimum() { + return minimum; + } + + public Double getMultipleOf() { + return multipleOf; + } + + /* (non-Javadoc) + * @see com.fasterxml.jackson.databind.jsonSchema.types.JsonSchema#getType() + */ + @Override + public JsonFormatTypes getType() + { + return JsonFormatTypes.NUMBER; + } + + @Override + public boolean isNumberSchema() { return true; } + + public void setExclusiveMaximum(Boolean exclusiveMaximum) { + this.exclusiveMaximum = exclusiveMaximum; + } + + public void setExclusiveMinimum(Boolean exclusiveMinimum) { + this.exclusiveMinimum = exclusiveMinimum; + } + + public void setMaximum(Double maximum) { + this.maximum = maximum; + } + + public void setMinimum(Double minimum) { + this.minimum = minimum; + } + + public void setMultipleOf(Double multipleOf) { + this.multipleOf = multipleOf; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof NumberSchema)) return false; + return _equals((NumberSchema) obj); + } + + protected boolean _equals(NumberSchema that) + { + return equals(getExclusiveMaximum(), that.getExclusiveMaximum()) && + equals(getExclusiveMinimum(), that.getExclusiveMinimum()) && + equals(getMaximum(), that.getMaximum()) && + equals(getMinimum(), that.getMinimum()) && + equals(getMultipleOf(), that.getMultipleOf()) && + super._equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ObjectSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ObjectSchema.java new file mode 100644 index 00000000..2c7fbbd3 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ObjectSchema.java @@ -0,0 +1,315 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * This type represents a {@link JsonSchema} as an object type + * @author jphelan + */ +public class ObjectSchema extends ContainerTypeSchema +{ + /** + * This attribute defines a jsonSchema for all properties that are not + * explicitly defined in an object type definition. If specified, the value + * MUST be a jsonSchema or a boolean. If false is provided, no additional + * properties are allowed beyond the properties defined in the jsonSchema. The + * default value is an empty jsonSchema which allows any value for additional + * properties. + */ + @JsonProperty + private AdditionalProperties additionalProperties; + + /** + * This attribute is an object that defines the requirements of a property + * on an instance object. If an object instance has a property with the same + * name as a property in this attribute's object, then the instance must be + * valid against the attribute's property value + */ + @JsonProperty + private Map dependencies; + + /** + * + This attribute is an object that defines the jsonSchema for a set of property + * names of an object instance. The name of each property of this + * attribute's object is a regular expression pattern in the ECMA 262/Perl 5 + * format, while the value is a jsonSchema. If the pattern matches the name of a + * property on the instance object, the value of the instance's property + * MUST be valid against the pattern name's jsonSchema value. + */ + @JsonProperty + private Map patternProperties; + + /** + * This attribute is an object with property definitions that define the + * valid values of instance object property values. When the instance value + * is an object, the property values of the instance object MUST conform to + * the property definitions in this object. In this object, each property + * definition's value MUST be a jsonSchema, and the property's name MUST be the + * name of the instance property that it defines. The instance property + * value MUST be valid according to the jsonSchema from the property definition. + * Properties are considered unordered, the order of the instance properties + * MAY be in any order. + */ + @JsonProperty + private Map properties; + + public ObjectSchema() + { + dependencies = new LinkedHashMap(); + patternProperties = new LinkedHashMap(); + properties = new LinkedHashMap(); + } + + public boolean addSchemaDependency(String depender, JsonSchema parentMustMatch) { + dependencies.put(depender, parentMustMatch); + return dependencies.get(depender).equals(parentMustMatch); + } + + public boolean addSimpleDependency(String depender, String dependsOn) { + @SuppressWarnings("unchecked") + Set existingDependsOn = (Set) dependencies.get(depender); + if (existingDependsOn == null) { + existingDependsOn= new LinkedHashSet<>(); + dependencies.put(depender, existingDependsOn); + } + return existingDependsOn.add(dependsOn); + } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.OBJECT; + } + + @Override + public boolean isObjectSchema() { + return true; + } + + @Override + public ObjectSchema asObjectSchema() { + return this; + } + + public AdditionalProperties getAdditionalProperties() { + return additionalProperties; + } + + public Map getDependencies() { + return dependencies; + } + + public Map getPatternProperties() { + return patternProperties; + } + + public Map getProperties() { + return properties; + } + + public void putOptionalProperty(BeanProperty property, JsonSchema jsonSchema) { + jsonSchema.enrichWithBeanProperty(property); + properties.put(property.getName(), jsonSchema); + } + + public void putOptionalProperty(String name, JsonSchema jsonSchema) { + properties.put(name, jsonSchema); + } + + public JsonSchema putPatternProperty(String regex, JsonSchema value) { + return patternProperties.put(regex, value); + } + + public JsonSchema putProperty(BeanProperty property, JsonSchema value) { + value.setRequired(true); + value.enrichWithBeanProperty(property); + return properties.put(property.getName(), value); + } + + public JsonSchema putProperty(String name, JsonSchema value) { + value.setRequired(true); + return properties.put(name, value); + } + + public void rejectAdditionalProperties() { + additionalProperties = NoAdditionalProperties.instance; + } + + public void setAdditionalProperties( + AdditionalProperties additionalProperties) { + this.additionalProperties = additionalProperties; + } + + public void setDependencies(Map dependencies) { + this.dependencies = dependencies; + } + + public void setPatternProperties(Map patternProperties) { + this.patternProperties = patternProperties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof ObjectSchema)) return false; + return _equals((ObjectSchema) obj); + } + + protected boolean _equals(ObjectSchema that) + { + return equals(getAdditionalProperties(), that.getAdditionalProperties()) + && equals(getDependencies(), that.getDependencies()) + && equals(getPatternProperties(), that.getPatternProperties()) + && equals(getProperties(), that.getProperties()) + && super._equals(that); + } + + @JsonDeserialize(using = AdditionalPropertiesDeserializer.class) + public static abstract class AdditionalProperties { + // simple marker class, deserializer and concrete impl have all functionality + } + + public static abstract class Dependency { + // simple marker class, deserializer and concrete impl have all functionality + @JsonCreator + public Dependency jsonCreator() { + //KNOWN ISSUE: pending https://github.com/FasterXML/jackson-databind/issues/43 + /* 29-Dec-2015, tatu: ... very unlikely above will be implemented anytime soon... + */ + return null; + } + } + + public static class NoAdditionalProperties extends AdditionalProperties { + public final Boolean schema = false; + + protected NoAdditionalProperties() { } + + @Override + public boolean equals(Object obj) { + return obj instanceof NoAdditionalProperties; + } + + @JsonValue + public Boolean value() { + return schema; + } + + public static final NoAdditionalProperties instance = new NoAdditionalProperties(); + } + + + public static class SchemaAdditionalProperties extends AdditionalProperties + { + @JsonProperty + private JsonSchema jsonSchema; + + @Override + public boolean equals(Object obj) { + return (obj instanceof SchemaAdditionalProperties) + && JsonSchema.equals(getJsonSchema(), ((SchemaAdditionalProperties)obj).getJsonSchema()); + } + + @JsonValue + public JsonSchema getJsonSchema() { + return jsonSchema; + } + + public SchemaAdditionalProperties(JsonSchema jsonSchema) { + this.jsonSchema = jsonSchema; + } + } + + /** + * JsonSchema Dependency If the dependency value is a jsonSchema, then the instance + * object MUST be valid against the jsonSchema. + */ + public static class SchemaDependency extends Dependency { + + @JsonProperty(required = true) + private String depender; + + @JsonProperty(required = true) + private JsonSchema parentMustMatch; + + public SchemaDependency(String depender, JsonSchema parentMustMatch) { + this.depender = depender; + this.parentMustMatch = parentMustMatch; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SchemaDependency) { + SchemaDependency that = (SchemaDependency) obj; + return JsonSchema.equals(getDepender(), that.getDepender()) && + JsonSchema.equals(getParentMustMatch(), that.getParentMustMatch()); + } + return false; + } + + public String getDepender() { + return depender; + } + + public JsonSchema getParentMustMatch() { + return parentMustMatch; + } + } + + /** + * Simple Dependency If the dependency value is a string, then the instance + * object MUST have a property with the same name as the dependency value. + * If the dependency value is an array of strings, then the instance object + * MUST have a property with the same name as each string in the dependency + * value's array. + */ + public static class SimpleDependency extends Dependency { + + @JsonProperty(required = true) + private String depender; + + @JsonProperty(required = true) + private String dependsOn; + + public SimpleDependency(String depender, String dependsOn) { + this.depender = depender; + this.dependsOn = dependsOn; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SchemaDependency) { + SimpleDependency that = (SimpleDependency) obj; + return JsonSchema.equals(getDepender(), that.getDepender()) && + JsonSchema.equals(getDependsOn(), that.getDependsOn()); + } + return false; + } + + public String getDepender() { + return depender; + } + + public String getDependsOn() { + return dependsOn; + } + } + +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ReferenceSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ReferenceSchema.java new file mode 100644 index 00000000..34d067b1 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ReferenceSchema.java @@ -0,0 +1,50 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; + +/** + * This type represents an JSON reference to a {@link com.fasterxml.jackson.module.jsonSchema.JsonSchema}. + * @author adb + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.PROPERTY, property = "type") +public class ReferenceSchema extends SimpleTypeSchema +{ + @JsonProperty + protected String $ref; + + public ReferenceSchema(String ref) { + this.$ref = ref; + } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.OBJECT; + } + + @Override + public String get$ref() { + return $ref; + } + + @Override + public void set$ref(String $ref) { + this.$ref = $ref; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof ReferenceSchema)) return false; + return _equals((ReferenceSchema) obj); + } + + protected boolean _equals(ReferenceSchema that) + { + return equals($ref, that.$ref) + && super._equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/SimpleTypeSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/SimpleTypeSchema.java new file mode 100644 index 00000000..736be934 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/SimpleTypeSchema.java @@ -0,0 +1,93 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class encapsulates the functionality of {@link JsonSchema} simple types + * @author jphelan + */ +public abstract class SimpleTypeSchema extends JsonSchema +{ + /** + * This attribute defines the default value of the instance when the + * instance is undefined. + */ + protected String defaultdefault; + + /** + * This attribute is a string that provides a short description of the + * instance property. + */ + protected String title; + + /** + * This attribute is a URI that defines what the instance's URI MUST start with in order to validate. + */ + protected String pathStart; + + /** + * This attribute is a string that provides a links related to description of the + * instance property. + */ + protected LinkDescriptionObject[] links; + + @Override + public SimpleTypeSchema asSimpleTypeSchema() { + return this; + } + + public String getDefault() { + return defaultdefault; + } + + public String getTitle() { + return title; + } + + public String getPathStart() { + return pathStart; + } + + public LinkDescriptionObject[] getLinks() { + return links; + } + + public void setLinks(LinkDescriptionObject[] links) { + this.links = links; + } + + @Override + public boolean isSimpleTypeSchema() { + return true; + } + + public void setDefault(String defaultdefault) { + this.defaultdefault = defaultdefault; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setPathStart(String pathStart) { + this.pathStart = pathStart; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof SimpleTypeSchema)) return false; + return _equals((SimpleTypeSchema) obj); + } + + protected boolean _equals(SimpleTypeSchema that) + { + return equals(getDefault(), that.getDefault()) + && equals(getTitle(), that.getTitle()) + && equals(getPathStart(), that.getPathStart()) + && arraysEqual(getLinks(), that.getLinks()) + && super._equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/StringSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/StringSchema.java new file mode 100644 index 00000000..6531f3d9 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/StringSchema.java @@ -0,0 +1,85 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This represents a {@link JsonSchema} as a String + * @author jphelan + * + */ +public class StringSchema extends ValueTypeSchema +{ + /** this defines the maximum length of the string. */ + @JsonProperty + private Integer maxLength; + + /** this defines the minimum length of the string. */ + @JsonProperty + private Integer minLength; + + /** + * this provides a regular expression that a string instance MUST match in + * order to be valid. Regular expressions SHOULD follow the regular + * expression specification from ECMA 262/Perl 5 + */ + @JsonProperty + private String pattern; + + @Override + public StringSchema asStringSchema() { + return this; + } + + public Integer getMaxLength() { + return maxLength; + } + + public Integer getMinLength() { + return minLength; + } + + public String getPattern() { + return pattern; + } + + @Override + public JsonFormatTypes getType() { + return JsonFormatTypes.STRING; + } + + @Override + public boolean isStringSchema() { + return true; + } + + public void setMaxLength(Integer maxLength) { + this.maxLength = maxLength; + } + + public void setMinLength(Integer minLength) { + this.minLength = minLength; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof StringSchema)) return false; + return _equals((StringSchema) obj); + } + + protected boolean _equals(StringSchema that) + { + return equals(getMaxLength(), that.getMaxLength()) + && equals(getMinLength(), that.getMinLength()) + && equals(getPattern(), that.getPattern()) + && super.equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/UnionTypeSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/UnionTypeSchema.java new file mode 100644 index 00000000..0b494ef4 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/UnionTypeSchema.java @@ -0,0 +1,66 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class represents a {@link JsonSchema} as a Union Type Schema: + * "An array of two or more simple type definitions. Each + item in the array MUST be a simple type definition or a schema. + The instance value is valid if it is of the same type as one of + the simple type definitions, or valid by one of the schemas, in + + * @author jphelan + */ +public class UnionTypeSchema extends JsonSchema +{ + @JsonProperty + protected ValueTypeSchema[] elements; + + @Override + public boolean isUnionTypeSchema() { + return true; + } + + @Override + public UnionTypeSchema asUnionTypeSchema() { + return this; + } + + // Important: This is the Type Id, as defined by base class `JsonSchema` + @Override + public JsonFormatTypes getType() { + // 29-Dec-2015, tatu: As per [module-jsonSchema#90], can not return null. + // ... but, alas, there is no real suitable value to return. Just can not + // be null; but if not null, will result in wrong deserialization. +// return JsonFormatTypes.UNION; + return null; + } + + public ValueTypeSchema[] getElements() { + return elements; + } + + public void setElements(ValueTypeSchema[] elements) { + if (elements.length < 2) { + throw new IllegalArgumentException("Union Type Schemas must contain two or more Simple Type Schemas"); + } + this.elements = elements; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof UnionTypeSchema)) return false; + return _equals((UnionTypeSchema) obj); + } + + protected boolean _equals(UnionTypeSchema that) + { + return arraysEqual(getElements(), that.getElements()) + && super._equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ValueTypeSchema.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ValueTypeSchema.java new file mode 100644 index 00000000..2017a5a8 --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/types/ValueTypeSchema.java @@ -0,0 +1,96 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.types; + +import java.util.*; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; + +/** + * This class represents a {@link JsonSchema} + * A primitive type. + */ +public abstract class ValueTypeSchema extends SimpleTypeSchema +{ + /** + * This provides an enumeration of all possible values that are valid + for the instance property. This MUST be an array, and each item in + the array represents a possible value for the instance value. If + this attribute is defined, the instance value MUST be one of the + values in the array in order for the schema to be valid. Comparison + of enum values uses the same algorithm as defined in "uniqueItems" + (Section 5.15). + */ + @JsonProperty(value = "enum") + @JsonDeserialize(as = LinkedHashSet.class) + protected Set enums = new LinkedHashSet(); + + /** + * This property defines the type of data, content type, or microformat to + * be expected in the instance property values. A format attribute MAY be + * one of the values listed below, and if so, SHOULD adhere to the semantics + * describing for the format. A format SHOULD only be used to give meaning + * to primitive types (string, integer, number, or boolean). Validators MAY + * (but are not required to) validate that the instance values conform to a + * format. + * + * Additional custom formats MAY be created. These custom formats MAY be + * expressed as an URI, and this URI MAY reference a schema of that + *

+ * NOTE: serialization of `format` was fixed in Jackson 2.7; requires at least + * this version of databind + */ + @JsonProperty + protected JsonValueFormat format; + + /** + * @deprecated Since 2.7 + */ + @Deprecated + @Override + public ValueTypeSchema asValueSchemaSchema() { + return asValueTypeSchema(); + } + + /** + * @since 2.7 + */ + @Override + public ValueTypeSchema asValueTypeSchema() { return this; } + + public Set getEnums() { + return enums; + } + + public JsonValueFormat getFormat() { + return format; + } + + @Override + public boolean isValueTypeSchema() { return true; } + + public void setEnums(Set enums) { + this.enums = enums; + } + + public void setFormat(JsonValueFormat format) { + this.format = format; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) return true; + if (obj == null) return false; + if (!(obj instanceof ValueTypeSchema)) return false; + return _equals((ValueTypeSchema) obj); + } + + protected boolean _equals(ValueTypeSchema that) + { + return equals(getFormat(), that.getFormat()) + && equals(getEnums(), that.getEnums()) + && super._equals(that); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/validation/AnnotationConstraintResolver.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/validation/AnnotationConstraintResolver.java new file mode 100644 index 00000000..e284f9ff --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/validation/AnnotationConstraintResolver.java @@ -0,0 +1,96 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.validation; + +import com.fasterxml.jackson.databind.BeanProperty; + +import jakarta.validation.constraints.*; +import java.math.BigDecimal; + +/** + * @author cponomaryov + * + * @since 2.5 + */ +public class AnnotationConstraintResolver + extends ValidationConstraintResolver +{ + private Integer getMaxSize(BeanProperty prop) { + Size ann = getSizeAnnotation(prop); + if (ann != null) { + int value = ann.max(); + if (value != Integer.MAX_VALUE) { + return value; + } + } + return null; + } + + private Integer getMinSize(BeanProperty prop) { + Size ann = getSizeAnnotation(prop); + if (ann != null) { + int value = ann.min(); + if (value != 0) { + return value; + } + } + return null; + } + + @Override + public Integer getArrayMaxItems(BeanProperty prop) { + return getMaxSize(prop); + } + + @Override + public Integer getArrayMinItems(BeanProperty prop) { + return getMinSize(prop); + } + + @Override + public Double getNumberMaximum(BeanProperty prop) { + Max maxAnnotation = prop.getAnnotation(Max.class); + if (maxAnnotation != null) { + return (double) maxAnnotation.value(); + } + DecimalMax decimalMaxAnnotation = prop.getAnnotation(DecimalMax.class); + return decimalMaxAnnotation != null ? new BigDecimal(decimalMaxAnnotation.value()).doubleValue() : null; + } + + @Override + public Double getNumberMinimum(BeanProperty prop) { + Min minAnnotation = prop.getAnnotation(Min.class); + if (minAnnotation != null) { + return (double) minAnnotation.value(); + } + DecimalMin decimalMinAnnotation = prop.getAnnotation(DecimalMin.class); + return decimalMinAnnotation != null ? new BigDecimal(decimalMinAnnotation.value()).doubleValue() : null; + } + + @Override + public Integer getStringMaxLength(BeanProperty prop) { + return getMaxSize(prop); + } + + @Override + public Integer getStringMinLength(BeanProperty prop) { + return getMinSize(prop); + } + + @Override + public String getStringPattern(final BeanProperty prop) { + Pattern patternAnnotation = prop.getAnnotation(Pattern.class); + if (patternAnnotation != null) { + return patternAnnotation.regexp(); + } + return null; + } + + @Override + public Boolean getRequired(BeanProperty prop) { + NotNull notNull = prop.getAnnotation(NotNull.class); + return notNull != null ? true : null; + } + + private Size getSizeAnnotation(BeanProperty prop) { + return prop.getAnnotation(Size.class); + } +} diff --git a/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/validation/ValidationConstraintResolver.java b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/validation/ValidationConstraintResolver.java new file mode 100644 index 00000000..15ee394f --- /dev/null +++ b/jakarta/src/main/java/com/fasterxml/jackson/module/jsonSchema/jakarta/validation/ValidationConstraintResolver.java @@ -0,0 +1,32 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.validation; + +import com.fasterxml.jackson.databind.BeanProperty; + +/** + * Note: implementations should + * + * @author cponomaryov + * + * @since 2.5 NOTE: changed from interface (2.5 - 2.7) to abstract class in 2.8 + */ +public abstract class ValidationConstraintResolver +{ + public abstract Integer getArrayMaxItems(BeanProperty prop); + + public abstract Integer getArrayMinItems(BeanProperty prop); + + public abstract Double getNumberMaximum(BeanProperty prop); + + public abstract Double getNumberMinimum(BeanProperty prop); + + public abstract Integer getStringMaxLength(BeanProperty prop); + + public abstract Integer getStringMinLength(BeanProperty prop); + + public abstract String getStringPattern(BeanProperty prop); + + /** + * @since 2.7 + */ + public abstract Boolean getRequired(BeanProperty prop); +} diff --git a/src/main/resources/META-INF/LICENSE b/jakarta/src/main/resources/META-INF/LICENSE similarity index 100% rename from src/main/resources/META-INF/LICENSE rename to jakarta/src/main/resources/META-INF/LICENSE diff --git a/jakarta/src/moditect/module-info.java b/jakarta/src/moditect/module-info.java new file mode 100644 index 00000000..04ddeb69 --- /dev/null +++ b/jakarta/src/moditect/module-info.java @@ -0,0 +1,15 @@ +// Generated 28-Mar-2019 using Moditect maven plugin +module com.fasterxml.jackson.module.jsonSchema.jakarta { + requires validation.api; + + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.databind; + + exports com.fasterxml.jackson.module.jsonSchema.jakarta; + exports com.fasterxml.jackson.module.jsonSchema.jakarta.annotation; + exports com.fasterxml.jackson.module.jsonSchema.jakarta.customProperties; + exports com.fasterxml.jackson.module.jsonSchema.jakarta.factories; + exports com.fasterxml.jackson.module.jsonSchema.jakarta.types; + exports com.fasterxml.jackson.module.jsonSchema.jakarta.validation; +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/CustomSchemaReadTest.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/CustomSchemaReadTest.java new file mode 100644 index 00000000..9c147a97 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/CustomSchemaReadTest.java @@ -0,0 +1,55 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; + +public class CustomSchemaReadTest extends SchemaTestBase +{ + @JsonTypeIdResolver(MyResolver.class) + public static class MySchema extends ObjectSchema { + } + + static class MyResolver extends JsonSchemaIdResolver + { + @Override + public String idFromValue(Object value) { + if (value instanceof MySchema) { + return "CUSTOM"; + } + return super.idFromValue(value); + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) { + if (value instanceof MySchema) { + return "CUSTOM"; + } + return super.idFromValueAndType(value, suggestedType); + } + + @Override + public JavaType typeFromId(DatabindContext ctxt, String id) { + if ("CUSTOM".equals(id)) { + return ctxt.constructType(MySchema.class); + } + return super.typeFromId(ctxt, id); + } + } + + // [module-jsonSchema#67] + public void testSchema() throws Exception + { + String input = "{ \"type\" : \"CUSTOM\" , \"id\" : \"7a2e8538-196b-423e-b714-13515848ec0c\" , \"description\" : \"My Schema\" , \"title\" : \"my-json-schema\" , \"properties\" : { \"myarray\" : { \"type\" : \"array\" , \"required\" : true , \"title\" : \"my property #2\" , \"items\" : { \"type\" : \"string\"} , \"maxItems\" : 5} , \"mystring\" : { \"type\" : \"string\" , \"required\" : true , \"title\" : \"my property #1\" , \"format\" : \"regex\" , \"pattern\" : \"\\\\w+\"} , \"myobject\" : { \"type\" : \"object\" , \"required\" : true , \"title\" : \"my property #3\" , \"properties\" : { \"subprop\" : { \"type\" : \"string\" , \"required\" : true , \"title\" : \"sub property #1\" , \"format\" : \"regex\" , \"pattern\" : \"\\\\w{3}\"}}}}}"; + + ObjectMapper mapper = new ObjectMapper(); + + // ObjectSchema schema = mapper.readValue(json, ObjectSchema.class); // works + MySchema schema = mapper.readValue(input, MySchema.class); // fails + + String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema); + assertNotNull(json); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/EnumGenerationTest.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/EnumGenerationTest.java new file mode 100644 index 00000000..5c3d215d --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/EnumGenerationTest.java @@ -0,0 +1,124 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class EnumGenerationTest extends SchemaTestBase +{ + public enum Enumerated { + A, B, C; + + // add this; should NOT matter but... + @Override + public String toString() { + return "ToString:"+name(); + } + } + + public static class LetterBean { + + public Enumerated letter; + } + + // for [jsonSchema#57] + public enum EnumViaJsonValue { + A, B, C; + + @JsonValue + public String asJson() { return name().toLowerCase(); } + } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + private final ObjectMapper MAPPER = new ObjectMapper(); + JsonSchemaGenerator SCHEMA_GEN = new JsonSchemaGenerator(MAPPER); + + public void testEnumDefault() throws Exception + { + JsonSchema jsonSchema = SCHEMA_GEN.generateSchema(LetterBean.class); + @SuppressWarnings("unchecked") + Map result = (Map) MAPPER.convertValue(jsonSchema, Map.class); + assertNotNull(result); + assertTrue(jsonSchema.isObjectSchema()); + assertEquals(expectedAsMap(false), result); + } + + public void testEnumWithToString() throws Exception + { + final ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + + JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper); + JsonSchema jsonSchema = generator.generateSchema(LetterBean.class); + @SuppressWarnings("unchecked") + Map result = (Map) mapper.convertValue(jsonSchema, Map.class); + assertNotNull(result); + assertTrue(jsonSchema.isObjectSchema()); + Map exp = expectedAsMap(true); + if (!exp.equals(result)) { + String expJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(exp); + String actJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result); + fail("Different JSON: expected:\n"+expJson+"\ngot:\n"+actJson); + } + } + + @SuppressWarnings("serial") + private Map expectedAsMap(final boolean useToString) + { + return new LinkedHashMap() { + { + put("type", "object"); + put("id", "urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:EnumGenerationTest:LetterBean"); + put("properties", + new LinkedHashMap() { + { + put("letter", + new LinkedHashMap() { + { + put("type", "string"); + put("enum", new ArrayList() { + { + add(useToString ? "ToString:A" : "A"); + add(useToString ? "ToString:B" : "B"); + add(useToString ? "ToString:C" : "C"); + } + }); + } + }); + } + }); + } + }; + } + + // for [jsonSchema#57] + @SuppressWarnings("unchecked") + public void testEnumWithJsonValue() throws Exception + { + JsonSchema schema = SCHEMA_GEN.generateSchema(EnumViaJsonValue.class); + + Map result = (Map) MAPPER.convertValue(schema, Map.class); + assertEquals("string", result.get("type")); + + Object values = result.get("enum"); + if (values == null) { + fail("Expected 'enum' entry, not found; schema: "+ MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(result)); + } + assertNotNull(values); + assertTrue(values instanceof List); + List enumValues = (List) values; + assertEquals(3, enumValues.size()); + assertEquals("a", enumValues.get(0)); + assertEquals("b", enumValues.get(1)); + assertEquals("c", enumValues.get(2)); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/EnumSchemaTest.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/EnumSchemaTest.java new file mode 100644 index 00000000..51e45ee9 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/EnumSchemaTest.java @@ -0,0 +1,87 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ValueTypeSchema; +import java.util.Iterator; +import java.util.Set; + +import com.fasterxml.jackson.databind.*; + +// for [module-jsonSchema#77]; problems (de)serializing enum values +public class EnumSchemaTest extends TestBase +{ + static enum MyEnum { FOO, BAR } + /* + static class MyClass { + public MyEnum[] myEnumField; + } + */ + + public void testEnumArrayDeserialization() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + mapper.acceptJsonFormatVisitor(mapper.constructType(MyEnum[].class), visitor); + JsonSchema schema = visitor.finalSchema(); + + // serialize schema: + String serializedSchema = mapper.writeValueAsString(schema); +// System.out.println("Schema1: " + serializedSchema); + assertTrue(serializedSchema.contains("FOO")); + + // deserialize the schema + JsonSchema schema2 = mapper.readValue(serializedSchema, JsonSchema.class); + + // verify + assertTrue(schema2.isArraySchema()); + JsonSchema itemSchema = ((ArraySchema) + schema2).getItems().asSingleItems().getSchema(); + assertTrue(itemSchema.isStringSchema()); + Set enums = ((ValueTypeSchema)itemSchema).getEnums(); + + /* + System.out.println("Schema2: " + mapper.writeValueAsString(schema2)); + System.out.println("Deserialized enums: " + enums); + */ + + assertTrue(enums.contains("FOO")); + } + + public void testEnumArrayDeserializationOrdering() throws Exception { + final String jsonSchema = "{\n" + + " \"type\": \"object\",\n" + + " \"id\": \"https://foo.bar/wibble\",\n" + + " \"$schema\": \"http://json-schema.org/draft-03/schema#\",\n" + + " \"properties\": {\n" + + " \"testOptions\": {\n" + + " \"type\": \"array\",\n" + + " \"id\": \"testOptions\",\n" + + " \"required\":true,\n" + + " \"items\": {\n" + + " \"type\": \"string\",\n" + + " \"enum\": [\n" + + " \"Section 1 'Macaroni and Cheese'\",\n" + + " \"Section 2 'Spaghetti and Meatballs'\",\n" + + " \"Section 3 'Fish and Chips'\",\n" + + " \"Section 4 'Sausage and Mash'\"\n" + + " ]\n" + + " },\n" + + " \"minItems\": 1\n" + + " }\n" + + " }\n" + + "}"; + + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(jsonSchema); + JsonSchema deserialized = mapper.convertValue(jsonNode, JsonSchema.class); + + ArraySchema testOptionsSchema = deserialized.asObjectSchema().getProperties().get("testOptions").asArraySchema(); + ValueTypeSchema testOptionItemsSchema = testOptionsSchema.getItems().asSingleItems().getSchema().asValueTypeSchema(); + Iterator enumSet = testOptionItemsSchema.getEnums().iterator(); + assertEquals("Expect enum options in order", "Section 1 'Macaroni and Cheese'", enumSet.next()); + assertEquals("Expect enum options in order", "Section 2 'Spaghetti and Meatballs'", enumSet.next()); + assertEquals("Expect enum options in order", "Section 3 'Fish and Chips'", enumSet.next()); + assertEquals("Expect enum options in order", "Section 4 'Sausage and Mash'", enumSet.next()); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/HyperSchemaFactoryWrapperTest.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/HyperSchemaFactoryWrapperTest.java new file mode 100644 index 00000000..9ccb8444 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/HyperSchemaFactoryWrapperTest.java @@ -0,0 +1,92 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.LinkDescriptionObject; +import com.fasterxml.jackson.module.jsonSchema.jakarta.annotation.JsonHyperSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.annotation.Link; +import com.fasterxml.jackson.module.jsonSchema.jakarta.customProperties.HyperSchemaFactoryWrapper; + +/** + * Created by mavarazy on 4/21/14. + */ +public class HyperSchemaFactoryWrapperTest extends SchemaTestBase { + + public class Pet { + public String genus; + } + + @JsonHyperSchema(pathStart = "/persons/", links = { + @Link(href = "{name}", rel = "self"), + @Link(href = "{name}/pet", rel = "pet", targetSchema = Pet.class) + }) + public class Person { + public String name; + public String hat; + } + + public void testSimpleHyperWithDefaultSchema() throws Exception { + HyperSchemaFactoryWrapper personVisitor = new HyperSchemaFactoryWrapper(); + personVisitor.setIgnoreDefaults(false); + ObjectMapper mapper = new ObjectMapper(); + + mapper.acceptJsonFormatVisitor(Person.class, personVisitor); + JsonSchema personSchema = personVisitor.finalSchema(); + + HyperSchemaFactoryWrapper petVisitor = new HyperSchemaFactoryWrapper(); + mapper.acceptJsonFormatVisitor(Pet.class, petVisitor); + JsonSchema petSchema = petVisitor.finalSchema(); + + assertTrue("schema should be an objectSchema.", personSchema.isObjectSchema()); + LinkDescriptionObject[] links = personSchema.asObjectSchema().getLinks(); + assertNotNull(links); + assertEquals(links.length, 2); + LinkDescriptionObject selfLink = links[0]; + assertEquals("/persons/{name}", selfLink.getHref()); + assertEquals("self", selfLink.getRel()); + assertEquals("application/json", selfLink.getEnctype()); + assertEquals("GET", selfLink.getMethod()); + + LinkDescriptionObject petLink = links[1]; + assertEquals("/persons/{name}/pet", petLink.getHref()); + assertEquals("pet", petLink.getRel()); + assertEquals("application/json", petLink.getEnctype()); + assertEquals("GET", petLink.getMethod()); + assertEquals(petSchema, petLink.getTargetSchema()); + } + + public void testSimpleHyperWithoutDefaultSchema() throws Exception { + final ObjectMapper mapper = new ObjectMapper(); + + /* + HyperSchemaFactoryWrapper personVisitor = new HyperSchemaFactoryWrapper(); + + mapper.acceptJsonFormatVisitor(Person.class, personVisitor); + JsonSchema personSchema = personVisitor.finalSchema(); + */ + JsonSchema personSchema = new JsonSchemaGenerator(mapper, + new HyperSchemaFactoryWrapper()) + .generateSchema(Person.class); + + HyperSchemaFactoryWrapper petVisitor = new HyperSchemaFactoryWrapper(); + mapper.acceptJsonFormatVisitor(Pet.class, petVisitor); + JsonSchema petSchema = petVisitor.finalSchema(); + + assertTrue("schema should be an objectSchema.", personSchema.isObjectSchema()); + LinkDescriptionObject[] links = personSchema.asObjectSchema().getLinks(); + assertNotNull(links); + assertEquals(links.length, 2); + LinkDescriptionObject selfLink = links[0]; + assertEquals("/persons/{name}", selfLink.getHref()); + assertEquals("self", selfLink.getRel()); + assertEquals(null, selfLink.getEnctype()); + assertEquals(null, selfLink.getMethod()); + + LinkDescriptionObject petLink = links[1]; + assertEquals("/persons/{name}/pet", petLink.getHref()); + assertEquals("pet", petLink.getRel()); + assertEquals(null, petLink.getEnctype()); + assertEquals(null, petLink.getMethod()); + assertEquals(petSchema, petLink.getTargetSchema()); + } + +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/SchemaTestBase.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/SchemaTestBase.java new file mode 100644 index 00000000..f1cb6fc3 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/SchemaTestBase.java @@ -0,0 +1,217 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import static org.junit.Assert.assertArrayEquals; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; + +public abstract class SchemaTestBase + extends TestBase +{ + private final static Object SINGLETON_OBJECT = new Object(); + + /* + /********************************************************** + /* Shared helper classes + /********************************************************** + */ + + /** + * Simple wrapper around boolean types, usually to test value + * conversions or wrapping + */ + protected static class BooleanWrapper { + public Boolean b; + + @JsonCreator + public BooleanWrapper(Boolean value) { b = value; } + + @JsonValue public Boolean value() { return b; } + } + + protected static class IntWrapper { + public int i; + + public IntWrapper() { } + public IntWrapper(int value) { i = value; } + } + + /** + * Simple wrapper around String type, usually to test value + * conversions or wrapping + */ + protected static class StringWrapper { + public String str; + + public StringWrapper() { } + public StringWrapper(String value) { + str = value; + } + } + + protected static class ObjectWrapper { + private final Object object; + protected ObjectWrapper(final Object object) { + this.object = object; + } + public Object getObject() { return object; } + @JsonCreator + static ObjectWrapper jsonValue(final Object object) { + return new ObjectWrapper(object); + } + } + + protected static class ListWrapper + { + public List list; + + public ListWrapper(T... values) { + list = new ArrayList(); + for (T value : values) { + list.add(value); + } + } + } + + protected static class MapWrapper + { + public Map map; + + public MapWrapper(Map m) { + map = m; + } + } + + protected static class ArrayWrapper + { + public T[] array; + + public ArrayWrapper(T[] v) { + array = v; + } + } + + /** + * Enumeration type with sub-classes per value. + */ + protected enum EnumWithSubClass { + A { @Override public void foobar() { } } + ,B { @Override public void foobar() { } } + ; + + public abstract void foobar(); + } + + protected SchemaTestBase() { super(); } + + /* + /********************************************************** + /* Additional assert methods + /********************************************************** + */ + + private final static ObjectMapper SHARED_MAPPER = new ObjectMapper(); + + protected ObjectMapper objectMapper() { + return SHARED_MAPPER; + } + + protected ObjectWriter objectWriter() { + return SHARED_MAPPER.writer(); + } + + protected ObjectReader objectReader(Class cls) { + return SHARED_MAPPER.readerFor(cls); + } + + /* + /********************************************************** + /* Additional assert methods + /********************************************************** + */ + + protected void assertEquals(int[] exp, int[] act) + { + assertArrayEquals(exp, act); + } + + /** + * Helper method for verifying 3 basic cookie cutter cases; + * identity comparison (true), and against null (false), + * or object of different type (false) + */ + protected void assertStandardEquals(Object o) + { + assertTrue(o.equals(o)); + assertFalse(o.equals(null)); + assertFalse(o.equals(SINGLETON_OBJECT)); + // just for fun, let's also call hash code... + o.hashCode(); + } + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + @SuppressWarnings("unchecked") + protected Map writeAndMap(ObjectMapper m, Object value) + throws IOException + { + String str = m.writeValueAsString(value); + return (Map) m.readValue(str, Map.class); + } + + protected T readAndMapFromString(ObjectMapper m, String input, Class cls) + throws IOException + { + return (T) m.readValue("\""+input+"\"", cls); + } + + protected String serializeAsString(ObjectMapper m, Object value) + throws IOException + { + return m.writeValueAsString(value); + } + + protected String serializeAsString(Object value) + throws IOException + { + return serializeAsString(new ObjectMapper(), value); + } + + protected String asJSONObjectValueString(Object... args) + throws IOException + { + return asJSONObjectValueString(new ObjectMapper(), args); + } + + protected String asJSONObjectValueString(ObjectMapper m, Object... args) + throws IOException + { + LinkedHashMap map = new LinkedHashMap(); + for (int i = 0, len = args.length; i < len; i += 2) { + map.put(args[i], args[i+1]); + } + return m.writeValueAsString(map); + } + + protected String aposToQuotes(String json) { + return json.replace("'", "\""); + } + + protected TimeZone getUTCTimeZone() { + return TimeZone.getTimeZone("GMT"); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestBase.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestBase.java new file mode 100644 index 00000000..b1f5193b --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestBase.java @@ -0,0 +1,417 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.Arrays; + +import junit.framework.TestCase; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +//import static org.junit.Assert.*; + +public abstract class TestBase + extends TestCase +{ + /* + /********************************************************** + /* Some sample documents: + /********************************************************** + */ + + protected final static int SAMPLE_SPEC_VALUE_WIDTH = 800; + protected final static int SAMPLE_SPEC_VALUE_HEIGHT = 600; + protected final static String SAMPLE_SPEC_VALUE_TITLE = "View from 15th Floor"; + protected final static String SAMPLE_SPEC_VALUE_TN_URL = "http://www.example.com/image/481989943"; + protected final static int SAMPLE_SPEC_VALUE_TN_HEIGHT = 125; + protected final static String SAMPLE_SPEC_VALUE_TN_WIDTH = "100"; + protected final static int SAMPLE_SPEC_VALUE_TN_ID1 = 116; + protected final static int SAMPLE_SPEC_VALUE_TN_ID2 = 943; + protected final static int SAMPLE_SPEC_VALUE_TN_ID3 = 234; + protected final static int SAMPLE_SPEC_VALUE_TN_ID4 = 38793; + + protected final static String SAMPLE_DOC_JSON_SPEC = + "{\n" + +" \"Image\" : {\n" + +" \"Width\" : "+SAMPLE_SPEC_VALUE_WIDTH+",\n" + +" \"Height\" : "+SAMPLE_SPEC_VALUE_HEIGHT+"," + +"\"Title\" : \""+SAMPLE_SPEC_VALUE_TITLE+"\",\n" + +" \"Thumbnail\" : {\n" + +" \"Url\" : \""+SAMPLE_SPEC_VALUE_TN_URL+"\",\n" + +"\"Height\" : "+SAMPLE_SPEC_VALUE_TN_HEIGHT+",\n" + +" \"Width\" : \""+SAMPLE_SPEC_VALUE_TN_WIDTH+"\"\n" + +" },\n" + +" \"IDs\" : ["+SAMPLE_SPEC_VALUE_TN_ID1+","+SAMPLE_SPEC_VALUE_TN_ID2+","+SAMPLE_SPEC_VALUE_TN_ID3+","+SAMPLE_SPEC_VALUE_TN_ID4+"]\n" + +" }" + +"}" + ; + + /* + /********************************************************** + /* Helper classes (beans) + /********************************************************** + */ + + /** + * Sample class from Jackson tutorial ("JacksonInFiveMinutes") + */ + protected static class FiveMinuteUser { + public enum Gender { MALE, FEMALE }; + + public static class Name + { + private String _first, _last; + + public Name() { } + public Name(String f, String l) { + _first = f; + _last = l; + } + + public String getFirst() { return _first; } + public String getLast() { return _last; } + + public void setFirst(String s) { _first = s; } + public void setLast(String s) { _last = s; } + + @Override + public boolean equals(Object o) + { + if (o == this) return true; + if (o == null || o.getClass() != getClass()) return false; + Name other = (Name) o; + return _first.equals(other._first) && _last.equals(other._last); + } + } + + private Gender _gender; + private Name _name; + private boolean _isVerified; + private byte[] _userImage; + + public FiveMinuteUser() { } + + public FiveMinuteUser(String first, String last, boolean verified, Gender g, byte[] data) + { + _name = new Name(first, last); + _isVerified = verified; + _gender = g; + _userImage = data; + } + + public Name getName() { return _name; } + public boolean isVerified() { return _isVerified; } + public Gender getGender() { return _gender; } + public byte[] getUserImage() { return _userImage; } + + public void setName(Name n) { _name = n; } + public void setVerified(boolean b) { _isVerified = b; } + public void setGender(Gender g) { _gender = g; } + public void setUserImage(byte[] b) { _userImage = b; } + + @Override + public boolean equals(Object o) + { + if (o == this) return true; + if (o == null || o.getClass() != getClass()) return false; + FiveMinuteUser other = (FiveMinuteUser) o; + if (_isVerified != other._isVerified) return false; + if (_gender != other._gender) return false; + if (!_name.equals(other._name)) return false; + byte[] otherImage = other._userImage; + if (otherImage.length != _userImage.length) return false; + for (int i = 0, len = _userImage.length; i < len; ++i) { + if (_userImage[i] != otherImage[i]) { + return false; + } + } + return true; + } + } + + /* + /********************************************************** + /* High-level helpers + /********************************************************** + */ + + protected void verifyJsonSpecSampleDoc(JsonParser jp, boolean verifyContents) + throws IOException + { + verifyJsonSpecSampleDoc(jp, verifyContents, true); + } + + protected void verifyJsonSpecSampleDoc(JsonParser jp, boolean verifyContents, + boolean requireNumbers) + throws IOException + { + if (!jp.hasCurrentToken()) { + jp.nextToken(); + } + assertToken(JsonToken.START_OBJECT, jp.getCurrentToken()); // main object + + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Image' + if (verifyContents) { + verifyFieldName(jp, "Image"); + } + + assertToken(JsonToken.START_OBJECT, jp.nextToken()); // 'image' object + + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Width' + if (verifyContents) { + verifyFieldName(jp, "Width"); + } + + verifyIntToken(jp.nextToken(), requireNumbers); + if (verifyContents) { + verifyIntValue(jp, SAMPLE_SPEC_VALUE_WIDTH); + } + + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Height' + if (verifyContents) { + verifyFieldName(jp, "Height"); + } + + verifyIntToken(jp.nextToken(), requireNumbers); + if (verifyContents) { + verifyIntValue(jp, SAMPLE_SPEC_VALUE_HEIGHT); + } + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Title' + if (verifyContents) { + verifyFieldName(jp, "Title"); + } + assertToken(JsonToken.VALUE_STRING, jp.nextToken()); + assertEquals(SAMPLE_SPEC_VALUE_TITLE, getAndVerifyText(jp)); + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Thumbnail' + if (verifyContents) { + verifyFieldName(jp, "Thumbnail"); + } + + assertToken(JsonToken.START_OBJECT, jp.nextToken()); // 'thumbnail' object + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Url' + if (verifyContents) { + verifyFieldName(jp, "Url"); + } + assertToken(JsonToken.VALUE_STRING, jp.nextToken()); + if (verifyContents) { + assertEquals(SAMPLE_SPEC_VALUE_TN_URL, getAndVerifyText(jp)); + } + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Height' + if (verifyContents) { + verifyFieldName(jp, "Height"); + } + verifyIntToken(jp.nextToken(), requireNumbers); + if (verifyContents) { + verifyIntValue(jp, SAMPLE_SPEC_VALUE_TN_HEIGHT); + } + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'Width' + if (verifyContents) { + verifyFieldName(jp, "Width"); + } + // Width value is actually a String in the example + assertToken(JsonToken.VALUE_STRING, jp.nextToken()); + if (verifyContents) { + assertEquals(SAMPLE_SPEC_VALUE_TN_WIDTH, getAndVerifyText(jp)); + } + + assertToken(JsonToken.END_OBJECT, jp.nextToken()); // 'thumbnail' object + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); // 'IDs' + assertToken(JsonToken.START_ARRAY, jp.nextToken()); // 'ids' array + verifyIntToken(jp.nextToken(), requireNumbers); // ids[0] + if (verifyContents) { + verifyIntValue(jp, SAMPLE_SPEC_VALUE_TN_ID1); + } + verifyIntToken(jp.nextToken(), requireNumbers); // ids[1] + if (verifyContents) { + verifyIntValue(jp, SAMPLE_SPEC_VALUE_TN_ID2); + } + verifyIntToken(jp.nextToken(), requireNumbers); // ids[2] + if (verifyContents) { + verifyIntValue(jp, SAMPLE_SPEC_VALUE_TN_ID3); + } + verifyIntToken(jp.nextToken(), requireNumbers); // ids[3] + if (verifyContents) { + verifyIntValue(jp, SAMPLE_SPEC_VALUE_TN_ID4); + } + assertToken(JsonToken.END_ARRAY, jp.nextToken()); // 'ids' array + + assertToken(JsonToken.END_OBJECT, jp.nextToken()); // 'image' object + + assertToken(JsonToken.END_OBJECT, jp.nextToken()); // main object + } + + private void verifyIntToken(JsonToken t, boolean requireNumbers) + { + if (t == JsonToken.VALUE_NUMBER_INT) { + return; + } + if (requireNumbers) { // to get error + assertToken(JsonToken.VALUE_NUMBER_INT, t); + } + // if not number, must be String + if (t != JsonToken.VALUE_STRING) { + fail("Expected INT or STRING value, got "+t); + } + } + + protected void verifyFieldName(JsonParser jp, String expName) + throws IOException + { + assertEquals(expName, jp.getText()); + assertEquals(expName, jp.getCurrentName()); + } + + protected void verifyIntValue(JsonParser jp, long expValue) + throws IOException + { + // First, via textual + assertEquals(String.valueOf(expValue), jp.getText()); + } + + /** + * Method that checks whether Unit tests appear to run from Ant build + * scripts. + * + * @since 1.6 + */ + protected static boolean runsFromAnt() { + return "true".equals(System.getProperty("FROM_ANT")); + } + + /* + /********************************************************** + /* Parser/generator construction + /********************************************************** + */ + + protected JsonParser createParserUsingReader(String input) + throws IOException, JsonParseException + { + return createParserUsingReader(new JsonFactory(), input); + } + + protected JsonParser createParserUsingReader(JsonFactory f, String input) + throws IOException, JsonParseException + { + return f.createParser(new StringReader(input)); + } + + protected JsonParser createParserUsingStream(String input, String encoding) + throws IOException, JsonParseException + { + return createParserUsingStream(new JsonFactory(), input, encoding); + } + + protected JsonParser createParserUsingStream(JsonFactory f, + String input, String encoding) + throws IOException, JsonParseException + { + + /* 23-Apr-2008, tatus: UTF-32 is not supported by JDK, have to + * use our own codec too (which is not optimal since there's + * a chance both encoder and decoder might have bugs, but ones + * that cancel each other out or such) + */ + byte[] data; + if (encoding.equalsIgnoreCase("UTF-32")) { + data = encodeInUTF32BE(input); + } else { + data = input.getBytes(encoding); + } + InputStream is = new ByteArrayInputStream(data); + return f.createParser(is); + } + + /* + /********************************************************** + /* Additional assertion methods + /********************************************************** + */ + + protected void assertToken(JsonToken expToken, JsonToken actToken) + { + if (actToken != expToken) { + fail("Expected token "+expToken+", current token "+actToken); + } + } + + protected void assertToken(JsonToken expToken, JsonParser jp) + { + assertToken(expToken, jp.getCurrentToken()); + } + + protected void assertType(Object ob, Class expType) + { + if (ob == null) { + fail("Expected an object of type "+expType.getName()+", got null"); + } + Class cls = ob.getClass(); + if (!expType.isAssignableFrom(cls)) { + fail("Expected type "+expType.getName()+", got "+cls.getName()); + } + } + + protected void verifyException(Throwable e, String... matches) + { + String msg = e.getMessage(); + String lmsg = (msg == null) ? "" : msg.toLowerCase(); + for (String match : matches) { + String lmatch = match.toLowerCase(); + if (lmsg.indexOf(lmatch) >= 0) { + return; + } + } + fail("Expected an exception with one of substrings ("+Arrays.asList(matches)+"): got one with message \""+msg+"\""); + } + + /** + * Method that gets textual contents of the current token using + * available methods, and ensures results are consistent, before + * returning them + */ + protected String getAndVerifyText(JsonParser jp) + throws IOException, JsonParseException + { + // Ok, let's verify other accessors + int actLen = jp.getTextLength(); + char[] ch = jp.getTextCharacters(); + String str2 = new String(ch, jp.getTextOffset(), actLen); + String str = jp.getText(); + + if (str.length() != actLen) { + fail("Internal problem (jp.token == "+jp.getCurrentToken()+"): jp.getText().length() ['"+str+"'] == "+str.length()+"; jp.getTextLength() == "+actLen); + } + assertEquals("String access via getText(), getTextXxx() must be the same", str, str2); + + return str; + } + + /* + /********************************************************** + /* And other helpers + /********************************************************** + */ + + protected byte[] encodeInUTF32BE(String input) + { + int len = input.length(); + byte[] result = new byte[len * 4]; + int ptr = 0; + for (int i = 0; i < len; ++i, ptr += 4) { + char c = input.charAt(i); + result[ptr] = result[ptr+1] = (byte) 0; + result[ptr+2] = (byte) (c >> 8); + result[ptr+3] = (byte) c; + } + return result; + } + + public String quote(String str) { + return '"'+str+'"'; + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestCyclic.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestCyclic.java new file mode 100644 index 00000000..fd912cca --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestCyclic.java @@ -0,0 +1,94 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; +import java.util.Map; + +public class TestCyclic extends SchemaTestBase +{ + // [Issue#4] + @JsonPropertyOrder({"next", "name"}) + public class Loop { + public String name; + public Loop next; + } + + public class ListLoop { + public List list; + } + + public class MapLoop { + public Map map; + } + + public class OuterLoop { + public InnerLoop inner; + } + + public class InnerLoop { + public OuterLoop outer; + } + + private final ObjectMapper MAPPER = objectMapper(); + + // [Issue#4] + public void testSimpleCyclic() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema schema = generator.generateSchema(Loop.class); + + String json = MAPPER.writeValueAsString(schema); + String EXP = "{\"type\":\"object\"," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:Loop\"," + + "\"properties\":{\"next\":{\"type\":\"object\"," + + "\"$ref\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:Loop\"}" + + ",\"name\":{\"type\":\"string\"}}}"; + assertEquals(aposToQuotes(EXP), json); + } + + public void testListCyclic() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema schema = generator.generateSchema(ListLoop.class); + + String json = MAPPER.writeValueAsString(schema); + String EXP = "{\"type\":\"object\"," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:ListLoop\"," + + "\"properties\":{\"list\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"$ref\":\"" + + "urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:ListLoop\"}}}}"; + + assertEquals(aposToQuotes(EXP), json); + } + + public void testMapCyclic() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema schema = generator.generateSchema(MapLoop.class); + + String json = MAPPER.writeValueAsString(schema); + String EXP = "{\"type\":\"object\"," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:MapLoop\"," + + "\"properties\":{\"map\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"object\"," + + "\"$ref\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:MapLoop\"}}}}"; + + assertEquals(aposToQuotes(EXP), json); + } + + public void testInnerOuterCyclic() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema schema = generator.generateSchema(OuterLoop.class); + + String json = MAPPER.writeValueAsString(schema); + String EXP = "{\"type\":\"object\"," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:OuterLoop\"," + + "\"properties\":{\"inner\":{\"type\":\"object\"," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:InnerLoop\"," + + "\"properties\":{\"outer\":{\"type\":\"object\"," + + "\"$ref\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestCyclic:OuterLoop\"}}}}}"; + + assertEquals(aposToQuotes(EXP), json); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestEquals.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestEquals.java new file mode 100644 index 00000000..fb2ebc7b --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestEquals.java @@ -0,0 +1,19 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NullSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; + +/** + * @author Yoann Rodière + */ +public class TestEquals extends SchemaTestBase { + + public void testEquals() throws Exception { + ObjectSchema schema1 = new ObjectSchema(); + ObjectSchema schema2 = new ObjectSchema(); + schema2.getProperties().put("property1", new NullSchema()); + + assertTrue(!schema1.equals(schema2)); + } + +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestGenerateJsonSchema.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestGenerateJsonSchema.java new file mode 100644 index 00000000..5cd1a6a0 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestGenerateJsonSchema.java @@ -0,0 +1,402 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("serial") +public class TestGenerateJsonSchema + extends SchemaTestBase +{ + @JsonPropertyOrder({"property1", "property2", "property3", "property4", "property5"}) + public static class SimpleBean + { + private int property1; + private String property2; + private String[] property3; + private Collection property4; + @JsonProperty(required = true) + private String property5; + + public int getProperty1() { + return property1; + } + + public void setProperty1(int property1) { + this.property1 = property1; + } + + public String getProperty2() { + return property2; + } + + public void setProperty2(String property2) { + this.property2 = property2; + } + + public String[] getProperty3() { + return property3; + } + + public void setProperty3(String[] property3) { + this.property3 = property3; + } + + public Collection getProperty4() { + return property4; + } + + public void setProperty4(Collection property4) { + this.property4 = property4; + } + + public String getProperty5() { + return property5; + } + + public void setProperty5(String property5) { + this.property5 = property5; + } + } + + public class TrivialBean { + + public String name; + } + + //@JsonSerializableSchema(id="myType") + public static class BeanWithId { + + public String value; + } + + @JsonFilter("filteredBean") + protected static class FilteredBean { + + @JsonProperty + private String secret = "secret"; + @JsonProperty + private String obvious = "obvious"; + + public String getSecret() { + return secret; + } + + public void setSecret(String s) { + secret = s; + } + + public String getObvious() { + return obvious; + } + + public void setObvious(String s) { + obvious = s; + } + } + public static FilterProvider secretFilterProvider = new SimpleFilterProvider() + .addFilter("filteredBean", SimpleBeanPropertyFilter.filterOutAllExcept(new String[]{"obvious"})); + + static class StringMap extends HashMap { + } + + static class DependencySchema { + @JsonProperty(required = true) + private String property2; + + public String getProperty2() { + return property2; + } + + public void setProperty2(String property2) { + this.property2 = property2; + } + } + + /* + /********************************************************** + /* Unit tests, success + /********************************************************** + */ + + private final ObjectMapper MAPPER = objectMapper(); + + private final ObjectMapper SORTED_MAPPER = JsonMapper.builder() + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .build(); + + + /** + * Test simple generation + */ + public void testGeneratingJsonSchema() throws Exception { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(SimpleBean.class); + + assertNotNull(jsonSchema); + + // test basic equality, and that equals() handles null, other obs + assertTrue(jsonSchema.equals(jsonSchema)); + assertFalse(jsonSchema.equals(null)); + assertFalse(jsonSchema.equals("foo")); + + assertTrue(jsonSchema.isObjectSchema()); + ObjectSchema object = jsonSchema.asObjectSchema(); + assertNotNull(object); + Map properties = object.getProperties(); + assertNotNull(properties); + JsonSchema prop1 = properties.get("property1"); + assertNotNull(prop1); + assertTrue(prop1.isIntegerSchema()); + assertNull(prop1.getRequired()); + assertNull(prop1.getReadonly()); + + JsonSchema prop2 = properties.get("property2"); + assertNotNull(prop2); + assertTrue(prop2.isStringSchema()); + assertNull(prop2.getRequired()); + assertNull(prop2.getReadonly()); + + JsonSchema prop3 = properties.get("property3"); + assertNotNull(prop3); + assertTrue(prop3.isArraySchema()); + assertNull(prop3.getRequired()); + assertNull(prop3.getReadonly()); + ArraySchema.Items items = prop3.asArraySchema().getItems(); + assertTrue(items.isSingleItems()); + JsonSchema itemType = items.asSingleItems().getSchema(); + assertNotNull(itemType); + assertTrue(itemType.isStringSchema()); + + JsonSchema prop4 = properties.get("property4"); + assertNotNull(prop4); + assertTrue(prop4.isArraySchema()); + assertNull(prop4.getRequired()); + assertNull(prop4.getReadonly()); + items = prop4.asArraySchema().getItems(); + assertTrue(items.isSingleItems()); + itemType = items.asSingleItems().getSchema(); + assertNotNull(itemType); + assertTrue(itemType.isNumberSchema()); + + JsonSchema prop5 = properties.get("property5"); + assertNotNull(prop5); + assertTrue(prop5.getRequired()); + assertNull(prop5.getReadonly()); + + } + + public void testGeneratingJsonSchemaWithFilters() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.setFilterProvider(secretFilterProvider); + JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper); + JsonSchema jsonSchema = generator.generateSchema(FilteredBean.class); + assertNotNull(jsonSchema); + assertTrue(jsonSchema.isObjectSchema()); + ObjectSchema object = jsonSchema.asObjectSchema(); + assertNotNull(object); + Map properties = object.getProperties(); + assertNotNull(properties); + JsonSchema obvious = properties.get("obvious"); + assertNotNull(obvious); + assertTrue(obvious.isStringSchema()); + assertNull(properties.get("secret")); + } + + /** + * Additional unit test for verifying that schema object itself can be + * properly serialized + */ + public void testSchemaSerialization() throws Exception { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(SimpleBean.class); + Map result = writeAndMap(MAPPER, jsonSchema); + assertNotNull(result); + // no need to check out full structure, just basics... + assertEquals("object", result.get("type")); + // only add 'required' if it is true... + assertNull(result.get("required")); + assertNotNull(result.get("properties")); + } + + /** + * Test for [JACKSON-454] + */ + public void testThatObjectsHaveNoItems() throws Exception { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(TrivialBean.class); + Map result = writeAndMap(MAPPER, jsonSchema); + // can we count on ordering being stable? I think this is true with current ObjectNode impl + // as perh [JACKSON-563]; 'required' is only included if true + assertFalse(result.containsKey("items")); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void testSchemaId() throws Exception { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(BeanWithId.class); + Map result = writeAndMap(MAPPER, jsonSchema); + + assertEquals(new HashMap() { + { + put("type", "object"); + put("id", "urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:BeanWithId"); + put("properties", + new HashMap() { + { + put("value", + new HashMap() { + { + put("type", "string"); + } + }); + } + }); + } + }, result); + } + + public void testSimpleMap() throws Exception { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(StringMap.class); + Map result = writeAndMap(MAPPER, jsonSchema); + assertNotNull(result); + + String mapSchemaStr = MAPPER.writeValueAsString(jsonSchema); + assertEquals("{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\"}}", mapSchemaStr); + } + + public void testSinglePropertyDependency() throws Exception { + final ObjectMapper mapper = SORTED_MAPPER; + JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper); + JsonSchema jsonSchema = generator.generateSchema(SimpleBean.class); + ((ObjectSchema) jsonSchema).addSimpleDependency("property1", "property2"); + + Map result = writeAndMap(mapper, jsonSchema); + assertNotNull(result); + + String schemaString = mapper.writeValueAsString(jsonSchema); + assertEquals("{\"type\":\"object\"," + + "\"dependencies\":{\"property1\":[\"property2\"]}," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:SimpleBean\"," + + "\"properties\":{\"property1\":{\"type\":\"integer\"}" + + ",\"property2\":{\"type\":\"string\"}," + + "\"property3\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}," + + "\"property4\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}," + + "\"property5\":{\"type\":\"string\",\"required\":true}}}", schemaString); + } + + public void testMultiplePropertyDependencies() throws Exception { + final ObjectMapper mapper = SORTED_MAPPER; + JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper); + JsonSchema jsonSchema = generator.generateSchema(SimpleBean.class); + ((ObjectSchema) jsonSchema).addSimpleDependency("property1", "property2"); + ((ObjectSchema) jsonSchema).addSimpleDependency("property1", "property3"); + ((ObjectSchema) jsonSchema).addSimpleDependency("property1", "property2"); + ((ObjectSchema) jsonSchema).addSimpleDependency("property2", "property3"); + + Map result = writeAndMap(mapper, jsonSchema); + assertNotNull(result); + + String schemaString = mapper.writeValueAsString(jsonSchema); + assertEquals("{\"type\":\"object\"," + + "\"dependencies\":{\"property1\":[\"property2\",\"property3\"],\"property2\":[\"property3\"]}," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:SimpleBean\"," + + "\"properties\":{\"property1\":{\"type\":\"integer\"}" + + ",\"property2\":{\"type\":\"string\"}," + + "\"property3\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}," + + "\"property4\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}," + + "\"property5\":{\"type\":\"string\",\"required\":true}}}", schemaString); + } + + public void testSchemaPropertyDependency() throws Exception { + final ObjectMapper mapper = SORTED_MAPPER; + JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper); + + // Given this dependency schema + JsonSchema schemaPropertyDependency = generator.generateSchema(DependencySchema.class); + + // Generate the SimpleBean and apply the schema dependency to one property + JsonSchema simpleBeanSchema = generator.generateSchema(SimpleBean.class); + ((ObjectSchema) simpleBeanSchema).addSchemaDependency("property1", schemaPropertyDependency); + + Map result = writeAndMap(mapper, simpleBeanSchema); + assertNotNull(result); + + // Test the generated value. + String schemaString = mapper.writeValueAsString(simpleBeanSchema); + assertEquals("{\"type\":\"object\"," + + "\"dependencies\":{\"property1\":{\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:DependencySchema\",\"properties\":{\"property2\":{\"type\":\"string\",\"required\":true}}}}," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:SimpleBean\"," + + "\"properties\":{\"property1\":{\"type\":\"integer\"}" + + ",\"property2\":{\"type\":\"string\"}," + + "\"property3\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}," + + "\"property4\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}," + + "\"property5\":{\"type\":\"string\",\"required\":true}}}", schemaString); + } + + public void testSchemaPropertyDependencies() throws Exception { + final ObjectMapper mapper = SORTED_MAPPER; + JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper); + + // Given this dependency schema + JsonSchema schemaPropertyDependency = generator.generateSchema(DependencySchema.class); + + // Generate the SimpleBean and apply the schema dependency to two properties + JsonSchema simpleBeanSchema = generator.generateSchema(SimpleBean.class); + ((ObjectSchema) simpleBeanSchema).addSchemaDependency("property1", schemaPropertyDependency); + ((ObjectSchema) simpleBeanSchema).addSchemaDependency("property3", schemaPropertyDependency); + + Map result = writeAndMap(mapper, simpleBeanSchema); + assertNotNull(result); + + // Test the generated value. + String schemaString = mapper.writeValueAsString(simpleBeanSchema); + assertEquals( + "{" + + "\"type\":\"object\"," + + "\"dependencies\":{" + + "\"property1\":{\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:DependencySchema\",\"properties\":{\"property2\":{\"type\":\"string\",\"required\":true}}}," + + "\"property3\":{\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:DependencySchema\",\"properties\":{\"property2\":{\"type\":\"string\",\"required\":true}}}}," + + "\"id\":\"urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestGenerateJsonSchema:SimpleBean\"," + + "\"properties\":{" + + "\"property1\":{\"type\":\"integer\"}" + + ",\"property2\":{\"type\":\"string\"}," + + "\"property3\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}," + + "\"property4\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}," + + "\"property5\":{\"type\":\"string\",\"required\":true}" + + "}" + + "}", schemaString); + } + + /* + /********************************************************** + /* Tests cases, error detection/handling + /********************************************************** + */ + + public void testInvalidCall() throws Exception { + // not ok to pass null + try { + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + MAPPER.acceptJsonFormatVisitor((JavaType) null, visitor); + fail("Should have failed"); + } catch (IllegalArgumentException iae) { + verifyException(iae, "type must be provided"); + } + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestJDKTypes.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestJDKTypes.java new file mode 100644 index 00000000..f03eb7bf --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestJDKTypes.java @@ -0,0 +1,32 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import java.math.*; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TestJDKTypes extends SchemaTestBase +{ + private final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Test simple generation for simple/primitive numeric types + */ + public void testSimpleNumbers() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema schema; + + schema = generator.generateSchema(Long.class); + assertEquals("{\"type\":\"integer\"}", MAPPER.writeValueAsString(schema)); + + schema = generator.generateSchema(BigInteger.class); + assertEquals("{\"type\":\"integer\"}", MAPPER.writeValueAsString(schema)); + + schema = generator.generateSchema(Double.class); + assertEquals("{\"type\":\"number\"}", MAPPER.writeValueAsString(schema)); + + schema = generator.generateSchema(BigDecimal.class); + assertEquals("{\"type\":\"number\"}", MAPPER.writeValueAsString(schema)); + + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestJsonValue.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestJsonValue.java new file mode 100644 index 00000000..6f59f296 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestJsonValue.java @@ -0,0 +1,103 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; +import java.util.Collection; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonValue; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TestJsonValue extends SchemaTestBase +{ + static class ContainerWithAsValue + { + private Leaf value; + + @JsonValue + public Leaf getValue() { return value; } + } + + static class Leaf { + public int value; + } + + static class Issue34Bean { + @JsonValue + public Collection getNames() { return null; } + } + + // For [module-jsonSchema#100] + static class MapViaJsonValue { + @JsonValue + public Map getMap() { + return null; + } + } + + /* + /********************************************************** + /* Unit tests, success + /********************************************************** + */ + + public void testJsonValueAnnotation() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + mapper.acceptJsonFormatVisitor(mapper.constructType(Leaf.class), visitor); + JsonSchema schemaExp = visitor.finalSchema(); + assertNotNull(schemaExp); + + visitor = new SchemaFactoryWrapper(); + mapper.acceptJsonFormatVisitor(mapper.constructType(ContainerWithAsValue.class), visitor); + JsonSchema schemaAct = visitor.finalSchema(); + assertNotNull(schemaAct); + + // these are minimal checks: + assertEquals(schemaExp.getType(), schemaAct.getType()); + assertEquals(schemaExp, schemaAct); + + // but let's require bit fuller match: + + // construction from bean results in an 'id' being set, whereas from @AsValue it doesn't. +// schemaExp.setId(null); + + String expStr = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schemaExp); + String actStr = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schemaAct); + + assertEquals(expStr, actStr); + } + + // For [module-jsonSchema#34] + public void testJsonValueForCollection() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + mapper.acceptJsonFormatVisitor(mapper.constructType(Issue34Bean.class), visitor); + JsonSchema schema = visitor.finalSchema(); + assertNotNull(schema); + + String schemaStr = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema); + assertNotNull(schemaStr); + } + + // For [module-jsonSchema#100] + public void testMapViaJsonValue() throws Exception { + ObjectMapper m = new ObjectMapper(); + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + m.acceptJsonFormatVisitor(m.constructType(MapViaJsonValue.class), visitor); + JsonSchema schema = visitor.finalSchema(); + assertType(schema, ObjectSchema.class); + ObjectSchema objectSchema = (ObjectSchema) schema; + + assertType(objectSchema.getAdditionalProperties(), ObjectSchema.SchemaAdditionalProperties.class); + ObjectSchema.SchemaAdditionalProperties additionalProperties = + (ObjectSchema.SchemaAdditionalProperties) objectSchema.getAdditionalProperties(); + assertType(additionalProperties.getJsonSchema(), StringSchema.class); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestPropertyOrderInSchema.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestPropertyOrderInSchema.java new file mode 100644 index 00000000..8f0ffaa4 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestPropertyOrderInSchema.java @@ -0,0 +1,94 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TestPropertyOrderInSchema extends SchemaTestBase { + + @JsonPropertyOrder({"c", "b", "a"}) + static class Bean { + + private int b; + private String c; + private String a; + + public int getB() { + return b; + } + + public void setB(int b) { + this.b = b; + } + + public String getC() { + return c; + } + + public void setC(String c) { + this.c = c; + } + + public String getA() { + return a; + } + + public void setA(String a) { + this.a = a; + } + } + + @JsonPropertyOrder(alphabetic = true) + static class Bean2 { + + private int b; + private String c; + private String a; + + public int getB() { + return b; + } + + public void setB(int b) { + this.b = b; + } + + public String getC() { + return c; + } + + public void setC(String c) { + this.c = c; + } + + public String getA() { + return a; + } + + public void setA(String a) { + this.a = a; + } + } + + /* + /********************************************************** + /* Unit tests + /********************************************************** + */ + + + public void testAnnotationOrder() throws Exception { + ObjectMapper MAPPER = objectMapper(); + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(Bean.class); + assertEquals(jsonSchema.asObjectSchema().getProperties().keySet().toString(), "[c, b, a]"); + } + + public void testAlphabeticOrder() throws Exception { + final ObjectMapper MAPPER = objectMapper(); + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(Bean2.class); + assertEquals(jsonSchema.asObjectSchema().getProperties().keySet().toString(), "[a, b, c]"); + } + +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestReadJsonSchema.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestReadJsonSchema.java new file mode 100644 index 00000000..7d450941 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestReadJsonSchema.java @@ -0,0 +1,239 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializable; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ObjectSchema; + +import java.io.IOException; +import java.util.*; + +/** + * Trivial test to ensure {@link JsonSchema} can be also deserialized + */ +public class TestReadJsonSchema + extends SchemaTestBase +{ + enum SchemaEnum { + YES, NO; + } + + @JsonPropertyOrder(alphabetic=true) + static class SchemableBasic + { + public String name; + public JsonSerializable someSerializable; + } + + @JsonPropertyOrder(alphabetic=true) + static class CompoundProperty { + public String a, b; + } + + static class SchemableArrays + { + public char[] nameBuffer; + // We'll include tons of stuff, just to force generation of schema + public boolean[] states; + public byte[] binaryData; + public short[] shorts; + public int[] ints; + public long[] longs; + public float[] floats; + public double[] doubles; + public Object[] objects; + public CompoundProperty[] basics; + } + + static class SchemabeLists + { + public ArrayList extra2; + public List extra; + public List basics; + // TODO this generates a $ref which can't be read +// public ArrayList basics2; + } + + static class GeneratesRef { + public CompoundProperty[] a; + // this property serializes as $ref to previous definition of CompoundProperty + public CompoundProperty[] b; + } + + static class SchemabeIterableOverObjects { + public Iterable iterableOhYeahBaby; + } + + static class SchemableMaps { + public Map> mapSizes; + } + + /* + /********************************************************** + /* Unit tests, success + /********************************************************** + */ + + private final ObjectMapper MAPPER = new ObjectMapper(); + { + MAPPER.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY); + } + + /** + * Verifies that a simple schema that is serialized can be deserialized back + * to equal schema instance + */ + public void testReadSimpleTypes() throws Exception { + _testSimple(SchemableBasic.class); + } + + public void testReadArrayTypes() throws Exception { + _testSimple(SchemableArrays.class); + } + + public void testReadListTypes() throws Exception { + _testSimple(SchemabeLists.class); + } + + public void testReadArray$ref() throws Exception { + _testSimple(GeneratesRef.class); + } + + public void testReadIterables() throws Exception { + _testSimple(SchemabeIterableOverObjects.class); + } + + public void testMapTypes() throws Exception { + _testSimple(SchemableMaps.class); + } + + public void testAdditionalItems() throws Exception { + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + MAPPER.acceptJsonFormatVisitor(MAPPER.constructType(SchemableArrays.class), visitor); + JsonSchema jsonSchema = visitor.finalSchema(); + assertNotNull(jsonSchema); + + assertTrue(jsonSchema.isObjectSchema()); + for (JsonSchema propertySchema : jsonSchema.asObjectSchema().getProperties().values()) + { + assertTrue(propertySchema.isArraySchema()); + propertySchema.asArraySchema().setAdditionalItems(new ArraySchema.NoAdditionalItems()); + } + + _testSimple("SchemableArrays - additionalItems", jsonSchema); + } + + static class SchemableEnumSet { + public EnumSet testEnums; + } + static class SchemableEnumMap { + public EnumMap> whatever; + } + + public void testStructuredEnumTypes() throws Exception { + _testSimple(SchemableEnumSet.class); + _testSimple(SchemableEnumMap.class); + } + + public void _testSimple(Class type) throws Exception + { + // Create a schema + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + MAPPER.acceptJsonFormatVisitor(MAPPER.constructType(type), visitor); + JsonSchema jsonSchema = visitor.finalSchema(); + assertNotNull(jsonSchema); + + _testSimple(type.getSimpleName(), jsonSchema); + } + + private void _testSimple(String name, JsonSchema jsonSchema) throws java.io.IOException { + // Need to do this twice so that $ref schemas + JsonSchema reread = writeAndRead(jsonSchema); + JsonSchema rereadAgain = writeAndRead(reread); + + assertEquals("Schemas for " + name + " differ", reread, rereadAgain); + } + + private JsonSchema writeAndRead(JsonSchema jsonSchema) throws IOException { + String asString = MAPPER.writeValueAsString(jsonSchema); + + assertNotNull(asString); + + return MAPPER.readValue(asString, JsonSchema.class); + } + + /** + * Verifies that false-valued and object-valued additional properties are + * deserialized properly + */ + public void testDeserializeFalseAndObjectAdditionalProperties() throws Exception + { + String schemaStr = "{\"type\":\"object\",\"properties\":{\"mapSizes\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"number\"}}},\"additionalProperties\":false}"; + JsonSchema schema = MAPPER.readValue(schemaStr, JsonSchema.class); + String newSchemaStr = MAPPER.writeValueAsString(schema); + assertEquals(schemaStr.replaceAll("\\s", "").length(), newSchemaStr.replaceAll("\\s", "").length()); + + JsonNode node = MAPPER.readTree(schemaStr); + JsonNode finalNode = MAPPER.readTree(newSchemaStr); + assertEquals(node, finalNode); + } + + /** + * Verifies that a true-valued additional property is deserialized properly + */ + public void testDeserializeTrueAdditionalProperties() throws Exception + { + String schemaStr = "{\"type\":\"object\",\"additionalProperties\":true}"; + ObjectSchema schema = MAPPER.readValue(schemaStr, ObjectSchema.class); + assertNull(schema.getAdditionalProperties()); + } + + /** + * Verifies that a true-valued additional property is deserialized properly + */ + public void testOneOf() throws Exception + { + String schemaStr = "{\n" + + " \"id\": \"http://some.site.somewhere/entry-schema#\",\n" + + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" + + " \"description\": \"schema for an fstab entry\",\n" + + " \"type\": \"object\",\n" + + //" \"required\": [ \"storage\" ],\n" + + " \"properties\": {\n" + + " \"storage\": {\n" + + " \"type\": \"object\",\n" + + " \"oneOf\": [\n" + + " { \"$ref\": \"#/definitions/diskDevice\" },\n" + + " { \"$ref\": \"#/definitions/diskUUID\" },\n" + + " { \"$ref\": \"#/definitions/nfs\" },\n" + + " { \"$ref\": \"#/definitions/tmpfs\" }\n" + + " ]\n" + + " },\n" + + " \"fstype\": {\n" + + " \"type\": \"object\",\n" + + " \"enum\": [ \"ext3\", \"ext4\", \"btrfs\" ]\n" + + " },\n" + + " \"options\": {\n" + + " \"type\": \"array\",\n" + + " \"minItems\": 1,\n" + + " \"items\": { \"type\": \"string\" },\n" + + " \"uniqueItems\": true\n" + + " },\n" + + " \"readonly\": { \"type\": \"boolean\" }\n" + + " }\n" + +// " \"definitions\": {\n" + +// " \"diskDevice\": {},\n" + +// " \"diskUUID\": {},\n" + +// " \"nfs\": {},\n" + +// " \"tmpfs\": {}\n" + +// " }\n" + + "}"; + ObjectSchema schema = MAPPER.readValue(schemaStr, ObjectSchema.class); + assertNotNull(schema.getProperties().get("storage").asObjectSchema().getOneOf()); + assertEquals(4,schema.getProperties().get("storage").asObjectSchema().getOneOf().size()); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestTypeGeneration.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestTypeGeneration.java new file mode 100644 index 00000000..396d5df9 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestTypeGeneration.java @@ -0,0 +1,33 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Date; + +public class TestTypeGeneration extends SchemaTestBase +{ + static class Issue14Bean + { + public Date date; + } + + /* + /********************************************************** + /* Unit tests + /********************************************************** + */ + + final ObjectMapper MAPPER = objectMapper(); + + // [Issue#14]: multiple type attributes + public void testCorrectType() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema jsonSchema = generator.generateSchema(Issue14Bean.class); + String json = MAPPER.writeValueAsString(jsonSchema).replace('"', '\''); + final String EXP = "{'type':'object'," + + "'id':'urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestTypeGeneration:Issue14Bean'," + + "'properties':{'date':{'type':'integer','format':'utc-millisec'}}}"; + assertEquals(EXP, json); + } + +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestUnwrapping.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestUnwrapping.java new file mode 100644 index 00000000..d62caad0 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestUnwrapping.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TestUnwrapping extends SchemaTestBase +{ + static class UnwrappingRoot + { + public int age; + + @JsonUnwrapped(prefix="name.") + public Name name; + } + + static class Name { + public String first, last; + } + + /* + /********************************************************** + /* Unit tests, success + /********************************************************** + */ + + private final ObjectMapper MAPPER = objectMapper(); + + public void testUnwrapping() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper); + JsonSchema schema = generator.generateSchema(UnwrappingRoot.class); + + String json = mapper.writeValueAsString(schema).replace('"', '\''); + + String EXP = "{'type':'object'," + + "'id':'urn:jsonschema:com:fasterxml:jackson:module:jsonSchema:jakarta:TestUnwrapping:UnwrappingRoot'," + + "'properties':{'age':{'type':'integer'},'name.first':{'type':'string'},'name.last':{'type':'string'}}}"; + assertEquals(EXP, json); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestVisitorContext.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestVisitorContext.java new file mode 100644 index 00000000..8d047893 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TestVisitorContext.java @@ -0,0 +1,100 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.module.jsonSchema.jakarta.factories.SchemaFactoryWrapper; + +public class TestVisitorContext extends SchemaTestBase +{ + static class Foo { + private String fooProp1; + private int fooProp2; + private Bar fooProp3; + + public String getFooProp1() { + return fooProp1; + } + + public void setFooProp1(String fooProp1) { + this.fooProp1 = fooProp1; + } + + public int getFooProp2() { + return fooProp2; + } + + public void setFooProp2(int fooProp2) { + this.fooProp2 = fooProp2; + } + + public Bar getFooProp3() { + return fooProp3; + } + + public void setFooProp3(Bar fooProp3) { + this.fooProp3 = fooProp3; + } + } + + static class Bar { + private String barProp1; + private int barProp2; + + public String getBarProp1() { + return barProp1; + } + + public void setBarProp1(String barProp1) { + this.barProp1 = barProp1; + } + + public int getBarProp2() { + return barProp2; + } + + public void setBarProp2(int barProp2) { + this.barProp2 = barProp2; + } + } + + static class Qwer { + private String qwerProp1; + private Bar qwerProp2; + + public String getQwerProp1() { + return qwerProp1; + } + + public void setQwerProp1(String qwerProp1) { + this.qwerProp1 = qwerProp1; + } + + public Bar getQwerProp2() { + return qwerProp2; + } + + public void setQwerProp2(Bar qwerProp2) { + this.qwerProp2 = qwerProp2; + } + } + + // for [jsonSchema#47] + public void testSchemaGeneration() throws Exception { + String schemaStr = generateSchema(Foo.class); + schemaStr = generateSchema(Qwer.class); + // ok, not very robust but has to do: + if (schemaStr.indexOf("$ref") >= 0) { + fail("Should not have $ref for type Bar (as per issue #47): "+schemaStr); + } + } + + private final static ObjectMapper MAPPER = new ObjectMapper(); + + private String generateSchema(Class clazz) throws Exception + { + MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true); + SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); + MAPPER.acceptJsonFormatVisitor(MAPPER.constructType(clazz), visitor); + return MAPPER.writeValueAsString(visitor.finalSchema()); + } + +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TitleSchemaFactoryWrapperTest.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TitleSchemaFactoryWrapperTest.java new file mode 100644 index 00000000..2852c387 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/TitleSchemaFactoryWrapperTest.java @@ -0,0 +1,38 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import junit.framework.TestCase; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.customProperties.TitleSchemaFactoryWrapper; + +public class TitleSchemaFactoryWrapperTest extends TestCase{ + + public class Pet { + public String genus; + } + + public class Person { + public String name; + public String hat; + public Pet pet; + } + + public void testAddingTitle() throws Exception + { + TitleSchemaFactoryWrapper visitor = new TitleSchemaFactoryWrapper(); + ObjectMapper mapper = new ObjectMapper(); + + mapper.acceptJsonFormatVisitor(Person.class, visitor); + JsonSchema schema = visitor.finalSchema(); + + assertTrue("schema should be an objectSchema.", schema.isObjectSchema()); + String title = schema.asObjectSchema().getTitle(); + assertNotNull(title); + assertTrue("schema should have a title", title.indexOf("Person") != -1); + JsonSchema schema2 = schema.asObjectSchema().getProperties().get("pet"); + assertTrue("schema should be an objectSchema.", schema2.isObjectSchema()); + String title2 = schema2.asObjectSchema().getTitle(); + assertNotNull(title2); + assertTrue("schema should have a title", title2.indexOf("Pet") != -1); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/ValidationSchemaFactoryWrapperTest.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/ValidationSchemaFactoryWrapperTest.java new file mode 100644 index 00000000..57e4a7b7 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/ValidationSchemaFactoryWrapperTest.java @@ -0,0 +1,365 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.types.StringSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.customProperties.ValidationSchemaFactoryWrapper; +import java.util.List; +import java.util.Map; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +/** + * @author cponomaryov + */ +public class ValidationSchemaFactoryWrapperTest extends SchemaTestBase { + + public static class ValidationBean { + + /* + /********************************************************** + /* Array fields + /********************************************************** + */ + + private List listWithoutConstraints; + + @Size(min = 1) + private List listWithMinSize; + + @Size(max = 2) + private List listWithMaxSize; + + @Size(min = 3, max = 4) + private List listWithMinAndMaxSize; + + /* + /********************************************************** + /* Number fields + /********************************************************** + */ + + private int numberWithoutConstraints; + + @Min(5) + private int numberWithMin; + + @DecimalMin("5.5") + private int numberWithDecimalMin; + + @Max(6) + private int numberWithMax; + + @DecimalMax("6.5") + private int numberWithDecimalMax; + + @Min(7) + @Max(8) + private int numberWithMinAndMax; + + @Min(9) + @DecimalMax("9.5") + private int numberWithMinAndDecimalMax; + + @DecimalMin("10.5") + @Max(11) + private int numberWithDecimalMinAndMax; + + @DecimalMin("11.5") + @DecimalMax("12.5") + private int numberWithDecimalMinAndDecimalMax; + + /* + /********************************************************** + /* String fields + /********************************************************** + */ + + private String stringWithoutConstraints; + + @Size(min = 13) + private String stringWithMinSize; + + @Size(max = 14) + private String stringWithMaxSize; + + @Size(min = 15, max = 16) + private String stringWithMinAndMaxSize; + + @Pattern(regexp = "[a-z]+") + private String stringWithPattern; + + /* + /********************************************************** + /* Nullable and not nullable fields + /********************************************************** + */ + + private String nullable; + + @NotNull + private String notNullable; + + public List getListWithoutConstraints() { + return listWithoutConstraints; + } + + public void setListWithoutConstraints(List listWithoutConstraints) { + this.listWithoutConstraints = listWithoutConstraints; + } + + public List getListWithMinSize() { + return listWithMinSize; + } + + public void setListWithMinSize(List listWithMinSize) { + this.listWithMinSize = listWithMinSize; + } + + public List getListWithMaxSize() { + return listWithMaxSize; + } + + public void setListWithMaxSize(List listWithMaxSize) { + this.listWithMaxSize = listWithMaxSize; + } + + public List getListWithMinAndMaxSize() { + return listWithMinAndMaxSize; + } + + public void setListWithMinAndMaxSize(List listWithMinAndMaxSize) { + this.listWithMinAndMaxSize = listWithMinAndMaxSize; + } + + public int getNumberWithoutConstraints() { + return numberWithoutConstraints; + } + + public void setNumberWithoutConstraints(int numberWithoutConstraints) { + this.numberWithoutConstraints = numberWithoutConstraints; + } + + public int getNumberWithMin() { + return numberWithMin; + } + + public void setNumberWithMin(int numberWithMin) { + this.numberWithMin = numberWithMin; + } + + public int getNumberWithDecimalMin() { + return numberWithDecimalMin; + } + + public void setNumberWithDecimalMin(int numberWithDecimalMin) { + this.numberWithDecimalMin = numberWithDecimalMin; + } + + public int getNumberWithMax() { + return numberWithMax; + } + + public void setNumberWithMax(int numberWithMax) { + this.numberWithMax = numberWithMax; + } + + public int getNumberWithDecimalMax() { + return numberWithDecimalMax; + } + + public void setNumberWithDecimalMax(int numberWithDecimalMax) { + this.numberWithDecimalMax = numberWithDecimalMax; + } + + public int getNumberWithMinAndMax() { + return numberWithMinAndMax; + } + + public void setNumberWithMinAndMax(int numberWithMinAndMax) { + this.numberWithMinAndMax = numberWithMinAndMax; + } + + public int getNumberWithMinAndDecimalMax() { + return numberWithMinAndDecimalMax; + } + + public void setNumberWithMinAndDecimalMax(int numberWithMinAndDecimalMax) { + this.numberWithMinAndDecimalMax = numberWithMinAndDecimalMax; + } + + public int getNumberWithDecimalMinAndMax() { + return numberWithDecimalMinAndMax; + } + + public void setNumberWithDecimalMinAndMax(int numberWithDecimalMinAndMax) { + this.numberWithDecimalMinAndMax = numberWithDecimalMinAndMax; + } + + public int getNumberWithDecimalMinAndDecimalMax() { + return numberWithDecimalMinAndDecimalMax; + } + + public void setNumberWithDecimalMinAndDecimalMax(int numberWithDecimalMinAndDecimalMax) { + this.numberWithDecimalMinAndDecimalMax = numberWithDecimalMinAndDecimalMax; + } + + public String getStringWithoutConstraints() { + return stringWithoutConstraints; + } + + public void setStringWithoutConstraints(String stringWithoutConstraints) { + this.stringWithoutConstraints = stringWithoutConstraints; + } + + public String getStringWithMinSize() { + return stringWithMinSize; + } + + public void setStringWithMinSize(String stringWithMinSize) { + this.stringWithMinSize = stringWithMinSize; + } + + public String getStringWithMaxSize() { + return stringWithMaxSize; + } + + public void setStringWithMaxSize(String stringWithMaxSize) { + this.stringWithMaxSize = stringWithMaxSize; + } + + public String getStringWithMinAndMaxSize() { + return stringWithMinAndMaxSize; + } + + public void setStringWithMinAndMaxSize(String stringWithMinAndMaxSize) { + this.stringWithMinAndMaxSize = stringWithMinAndMaxSize; + } + + public String getStringWithPattern() { + return stringWithPattern; + } + + public void setStringWithPattern(final String stringWithPattern) { + this.stringWithPattern = stringWithPattern; + } + + public String getNullable() { + return nullable; + } + + public void setNullable(String nullable) { + this.nullable = nullable; + } + + public String getNotNullable() { + return notNullable; + } + + public void setNotNullable(String notNullable) { + this.notNullable = notNullable; + } + } + + /* + /********************************************************** + /* Unit tests, success + /********************************************************** + */ + + private Object[][] listTestData() { + return new Object[][] {{"listWithoutConstraints", null, null}, + {"listWithMinSize", 1, null}, + {"listWithMaxSize", null, 2}, + {"listWithMinAndMaxSize", 3, 4}}; + } + + private Object[][] numberTestData() { + return new Object[][] {{"numberWithoutConstraints", null, null}, + {"numberWithMin", 5d, null}, + {"numberWithDecimalMin", 5.5, null}, + {"numberWithMax", null, 6d}, + {"numberWithDecimalMax", null, 6.5}, + {"numberWithMinAndMax", 7d, 8d}, + {"numberWithMinAndDecimalMax", 9d, 9.5}, + {"numberWithDecimalMinAndMax", 10.5, 11d}, + {"numberWithDecimalMinAndDecimalMax", 11.5, 12.5}}; + } + + private Object[][] stringTestData() { + return new Object[][] {{"stringWithoutConstraints", null, null}, + {"stringWithMinSize", 13, null}, + {"stringWithMaxSize", null, 14}, + {"stringWithMinAndMaxSize", 15, 16}}; + } + + private Object[][] stringPatternTestData() { + return new Object[][] {{"stringWithPattern", "[a-z]+"}, + {"stringWithoutConstraints", null}}; + } + + private Object[][] notNullTestData() { + return new Object[][] { + {"nullable", null}, + {"notNullable", true}}; + } + + /** + * Test set validation constraints + */ + public void testAddingValidationConstraints() throws Exception { + ValidationSchemaFactoryWrapper visitor = new ValidationSchemaFactoryWrapper(); + ObjectMapper mapper = new ObjectMapper(); + + mapper.acceptJsonFormatVisitor(ValidationBean.class, visitor); + JsonSchema jsonSchema = visitor.finalSchema(); + + assertNotNull("schema should not be null.", jsonSchema); + assertTrue("schema should be an objectSchema.", jsonSchema.isObjectSchema()); + Map properties = jsonSchema.asObjectSchema().getProperties(); + assertNotNull(properties); + for (Object[] testCase : listTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertTrue(propertySchema.isArraySchema()); + ArraySchema arraySchema = propertySchema.asArraySchema(); + assertEquals(testCase[1], arraySchema.getMinItems()); + assertEquals(testCase[2], arraySchema.getMaxItems()); + } + for (Object[] testCase : numberTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertTrue(propertySchema.isNumberSchema()); + NumberSchema numberSchema = propertySchema.asNumberSchema(); + assertEquals(testCase[1], numberSchema.getMinimum()); + assertEquals(testCase[2], numberSchema.getMaximum()); + } + for (Object[] testCase : stringTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertTrue(propertySchema.isStringSchema()); + StringSchema stringSchema = propertySchema.asStringSchema(); + assertEquals(testCase[1], stringSchema.getMinLength()); + assertEquals(testCase[2], stringSchema.getMaxLength()); + } + for (Object[] testCase : stringPatternTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertTrue(propertySchema.isStringSchema()); + StringSchema stringSchema = propertySchema.asStringSchema(); + assertEquals(testCase[1], stringSchema.getPattern()); + } + for (Object[] testCase : notNullTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertEquals(testCase[1], propertySchema.getRequired()); + } + } + +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/failing/MapTest.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/failing/MapTest.java new file mode 100644 index 00000000..ebbe8319 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/failing/MapTest.java @@ -0,0 +1,49 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.failing; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchemaGenerator; +import com.fasterxml.jackson.module.jsonSchema.jakarta.TestBase; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; + +// for [module-jsonSchema#89] +public class MapTest extends TestBase +{ + static class MapBean { + private Map counts; + + public void setCounts(Map counts) { + this.counts = counts; + } + + public Map getCounts() { + return counts; + } + } + + /* + /********************************************************** + /* Tests methods + /********************************************************** + */ + + private final ObjectMapper MAPPER = new ObjectMapper(); + + public void testSimpleMapKeyType89() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema schema; + + schema = generator.generateSchema(MapBean.class); + String json = MAPPER + .writerWithDefaultPrettyPrinter() + .writeValueAsString(schema); +// System.out.println(json); + + // !!! TODO: actually verify key type is retained + fail("Key type not preserved, contents: "+json); + +// assertEquals("{\"type\":\"integer\"}", json); + } +} diff --git a/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/failing/TestBinaryType.java b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/failing/TestBinaryType.java new file mode 100644 index 00000000..6df82881 --- /dev/null +++ b/jakarta/src/test/java/com/fasterxml/jackson/module/jsonSchema/jakarta/failing/TestBinaryType.java @@ -0,0 +1,31 @@ +package com.fasterxml.jackson.module.jsonSchema.jakarta.failing; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchemaGenerator; +import com.fasterxml.jackson.module.jsonSchema.jakarta.SchemaTestBase; + +public class TestBinaryType extends SchemaTestBase +{ + private final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Test simple generation for simple/primitive numeric types + */ + public void testBinaryType() throws Exception + { + JsonSchemaGenerator generator = new JsonSchemaGenerator(MAPPER); + JsonSchema schema; + + schema = generator.generateSchema(byte[].class); + + // Should be either an array of bytes, or, String with 'format' of "base64" + String json = MAPPER.writeValueAsString(schema); + + if (!json.equals(aposToQuotes("{'type':'array','items':{'type':'byte'}}"))) { + String pretty = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(schema); + fail("Should get 'array of bytes' or 'String as Base64', instead got: "+pretty); + } + } +} diff --git a/javax/pom.xml b/javax/pom.xml new file mode 100644 index 00000000..5b7c5ec8 --- /dev/null +++ b/javax/pom.xml @@ -0,0 +1,57 @@ + + + + + + + 4.0.0 + + com.fasterxml.jackson.module + jackson-module-jsonSchema-parent + 2.15.0-SNAPSHOT + + jackson-module-jsonSchema + jackson-module-jsonSchema + bundle + Add-on module for Jackson (http://jackson.codehaus.org) to support +JSON Schema (http://tools.ietf.org/html/draft-zyp-json-schema-03) version 3 generation. + + https://github.com/FasterXML/jackson-module-jsonSchema + + + + javax.validation + validation-api + 1.1.0.Final + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + + com/fasterxml/jackson/module/jsonSchema/failing/*.java + + + + + + org.moditect + moditect-maven-plugin + + + + de.jjohannes + gradle-module-metadata-maven-plugin + + + + + diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaGenerator.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaGenerator.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaGenerator.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaGenerator.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaIdResolver.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaIdResolver.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaIdResolver.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/JsonSchemaIdResolver.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/JsonHyperSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/JsonHyperSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/JsonHyperSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/JsonHyperSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/Link.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/Link.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/Link.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/annotation/Link.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/HyperSchemaFactoryWrapper.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/HyperSchemaFactoryWrapper.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/HyperSchemaFactoryWrapper.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/HyperSchemaFactoryWrapper.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/TitleSchemaFactoryWrapper.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/TitleSchemaFactoryWrapper.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/TitleSchemaFactoryWrapper.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/TitleSchemaFactoryWrapper.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/ValidationSchemaFactoryWrapper.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/ValidationSchemaFactoryWrapper.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/ValidationSchemaFactoryWrapper.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/ValidationSchemaFactoryWrapper.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/AnyVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/AnyVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/AnyVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/AnyVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ArrayVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ArrayVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ArrayVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ArrayVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/BooleanVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/BooleanVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/BooleanVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/BooleanVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/FormatVisitorFactory.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/FormatVisitorFactory.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/FormatVisitorFactory.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/FormatVisitorFactory.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/IntegerVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/IntegerVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/IntegerVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/IntegerVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaFactory.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaFactory.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaFactory.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaFactory.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaProducer.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaProducer.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaProducer.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/JsonSchemaProducer.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/MapVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/MapVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/MapVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/MapVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NullVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NullVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NullVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NullVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NumberVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NumberVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NumberVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/NumberVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/SchemaFactoryWrapper.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/SchemaFactoryWrapper.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/SchemaFactoryWrapper.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/SchemaFactoryWrapper.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StringVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StringVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StringVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StringVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StructuredTypeVisitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StructuredTypeVisitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StructuredTypeVisitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/StructuredTypeVisitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/Visitor.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/Visitor.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/Visitor.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/Visitor.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/VisitorContext.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/VisitorContext.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/VisitorContext.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/VisitorContext.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/WrapperFactory.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/WrapperFactory.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/WrapperFactory.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/WrapperFactory.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalItemsDeserializer.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalItemsDeserializer.java similarity index 90% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalItemsDeserializer.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalItemsDeserializer.java index 305b54aa..4f412ae4 100644 --- a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalItemsDeserializer.java +++ b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalItemsDeserializer.java @@ -9,9 +9,6 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.module.jsonSchema.JsonSchema; -import com.fasterxml.jackson.module.jsonSchema.types.AdditionalPropertiesDeserializer; -import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema; - /** * @author Yoann Rodière (adapted from {@link AdditionalPropertiesDeserializer}, by Ignacio del Valle Alles) */ diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalPropertiesDeserializer.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalPropertiesDeserializer.java similarity index 97% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalPropertiesDeserializer.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalPropertiesDeserializer.java index 214081ae..bad11c3c 100644 --- a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalPropertiesDeserializer.java +++ b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AdditionalPropertiesDeserializer.java @@ -1,51 +1,51 @@ -/* - * Copyright 2013 FasterXML. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.fasterxml.jackson.module.jsonSchema.types; - -import java.io.IOException; - -import com.fasterxml.jackson.core.*; - -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.module.jsonSchema.JsonSchema; - -/** - * @author Ignacio del Valle Alles - */ -public class AdditionalPropertiesDeserializer - extends JsonDeserializer -{ - @Override - public ObjectSchema.AdditionalProperties deserialize(JsonParser p, DeserializationContext ctxt) throws IOException - { - if (p.hasCurrentToken()) { - switch (p.getCurrentTokenId()) { - case JsonTokenId.ID_TRUE: - return null; // "additionalProperties":true is the default - case JsonTokenId.ID_FALSE: - return ObjectSchema.NoAdditionalProperties.instance; - case JsonTokenId.ID_START_OBJECT: - case JsonTokenId.ID_FIELD_NAME: - case JsonTokenId.ID_END_OBJECT: - // 29-Dec-2015, tatu: may need/want to use property value reader in future but for now: - JsonSchema innerSchema = ctxt.readValue(p, JsonSchema.class); - return new ObjectSchema.SchemaAdditionalProperties(innerSchema); - } - } - return ctxt.reportInputMismatch(this, -"additionalProperties nodes can only be of type boolean or object, got token of type: %s", p.getCurrentToken()); - } -} +/* + * Copyright 2013 FasterXML. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fasterxml.jackson.module.jsonSchema.types; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; + +/** + * @author Ignacio del Valle Alles + */ +public class AdditionalPropertiesDeserializer + extends JsonDeserializer +{ + @Override + public ObjectSchema.AdditionalProperties deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + if (p.hasCurrentToken()) { + switch (p.getCurrentTokenId()) { + case JsonTokenId.ID_TRUE: + return null; // "additionalProperties":true is the default + case JsonTokenId.ID_FALSE: + return ObjectSchema.NoAdditionalProperties.instance; + case JsonTokenId.ID_START_OBJECT: + case JsonTokenId.ID_FIELD_NAME: + case JsonTokenId.ID_END_OBJECT: + // 29-Dec-2015, tatu: may need/want to use property value reader in future but for now: + JsonSchema innerSchema = ctxt.readValue(p, JsonSchema.class); + return new ObjectSchema.SchemaAdditionalProperties(innerSchema); + } + } + return ctxt.reportInputMismatch(this, +"additionalProperties nodes can only be of type boolean or object, got token of type: %s", p.getCurrentToken()); + } +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AnySchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AnySchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AnySchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/AnySchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ArraySchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ArraySchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ArraySchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ArraySchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/BooleanSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/BooleanSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/BooleanSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/BooleanSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ContainerTypeSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ContainerTypeSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ContainerTypeSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ContainerTypeSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/HyperSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/HyperSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/HyperSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/HyperSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/IntegerSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/IntegerSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/IntegerSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/IntegerSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/LinkDescriptionObject.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/LinkDescriptionObject.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/LinkDescriptionObject.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/LinkDescriptionObject.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NullSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NullSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NullSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NullSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NumberSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NumberSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NumberSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/NumberSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ObjectSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ObjectSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ObjectSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ObjectSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ReferenceSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ReferenceSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ReferenceSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ReferenceSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/SimpleTypeSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/SimpleTypeSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/SimpleTypeSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/SimpleTypeSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/StringSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/StringSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/StringSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/StringSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/UnionTypeSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/UnionTypeSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/UnionTypeSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/UnionTypeSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ValueTypeSchema.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ValueTypeSchema.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ValueTypeSchema.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/types/ValueTypeSchema.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/AnnotationConstraintResolver.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/AnnotationConstraintResolver.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/AnnotationConstraintResolver.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/AnnotationConstraintResolver.java diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/ValidationConstraintResolver.java b/javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/ValidationConstraintResolver.java similarity index 100% rename from src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/ValidationConstraintResolver.java rename to javax/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/ValidationConstraintResolver.java diff --git a/javax/src/main/resources/META-INF/LICENSE b/javax/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000..6acf7548 --- /dev/null +++ b/javax/src/main/resources/META-INF/LICENSE @@ -0,0 +1,8 @@ +This copy of Jackson JSON processor databind module is licensed under the +Apache (Software) License, version 2.0 ("the License"). +See the License for details about distribution rights, and the +specific rights regarding derivate works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/src/moditect/module-info.java b/javax/src/moditect/module-info.java similarity index 100% rename from src/moditect/module-info.java rename to javax/src/moditect/module-info.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/CustomSchemaReadTest.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/CustomSchemaReadTest.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/CustomSchemaReadTest.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/CustomSchemaReadTest.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumGenerationTest.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumGenerationTest.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumGenerationTest.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumGenerationTest.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumSchemaTest.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumSchemaTest.java similarity index 98% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumSchemaTest.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumSchemaTest.java index 3d1c9bf5..1a657717 100644 --- a/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumSchemaTest.java +++ b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/EnumSchemaTest.java @@ -4,7 +4,6 @@ import java.util.Set; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.module.jsonSchema.JsonSchema; import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper; import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema; import com.fasterxml.jackson.module.jsonSchema.types.ValueTypeSchema; diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/HyperSchemaFactoryWrapperTest.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/HyperSchemaFactoryWrapperTest.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/HyperSchemaFactoryWrapperTest.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/HyperSchemaFactoryWrapperTest.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/SchemaTestBase.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/SchemaTestBase.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/SchemaTestBase.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/SchemaTestBase.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestBase.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestBase.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestBase.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestBase.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestCyclic.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestCyclic.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestCyclic.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestCyclic.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestEquals.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestEquals.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestEquals.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestEquals.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestGenerateJsonSchema.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestGenerateJsonSchema.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestGenerateJsonSchema.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestGenerateJsonSchema.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJDKTypes.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJDKTypes.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJDKTypes.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJDKTypes.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJsonValue.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJsonValue.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJsonValue.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestJsonValue.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestPropertyOrderInSchema.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestPropertyOrderInSchema.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestPropertyOrderInSchema.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestPropertyOrderInSchema.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestReadJsonSchema.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestReadJsonSchema.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestReadJsonSchema.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestReadJsonSchema.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestTypeGeneration.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestTypeGeneration.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestTypeGeneration.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestTypeGeneration.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestUnwrapping.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestUnwrapping.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestUnwrapping.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestUnwrapping.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestVisitorContext.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestVisitorContext.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TestVisitorContext.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestVisitorContext.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TitleSchemaFactoryWrapperTest.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TitleSchemaFactoryWrapperTest.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/TitleSchemaFactoryWrapperTest.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/TitleSchemaFactoryWrapperTest.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/ValidationSchemaFactoryWrapperTest.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/ValidationSchemaFactoryWrapperTest.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/ValidationSchemaFactoryWrapperTest.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/ValidationSchemaFactoryWrapperTest.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/MapTest.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/MapTest.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/MapTest.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/MapTest.java diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/TestBinaryType.java b/javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/TestBinaryType.java similarity index 100% rename from src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/TestBinaryType.java rename to javax/src/test/java/com/fasterxml/jackson/module/jsonSchema/failing/TestBinaryType.java diff --git a/pom.xml b/pom.xml index 11220e32..07cfda79 100644 --- a/pom.xml +++ b/pom.xml @@ -11,10 +11,10 @@ 2.15.0-SNAPSHOT com.fasterxml.jackson.module - jackson-module-jsonSchema - jackson-module-jsonSchema + jackson-module-jsonSchema-parent + jackson-module-jsonSchema-parent 2.15.0-SNAPSHOT - bundle + pom Add-on module for Jackson (http://jackson.codehaus.org) to support JSON Schema (http://tools.ietf.org/html/draft-zyp-json-schema-03) version 3 generation. @@ -38,6 +38,11 @@ JSON Schema (http://tools.ietf.org/html/draft-zyp-json-schema-03) version 3 gene + + javax + jakarta + + com.fasterxml.jackson.core @@ -51,11 +56,6 @@ JSON Schema (http://tools.ietf.org/html/draft-zyp-json-schema-03) version 3 gene com.fasterxml.jackson.core jackson-databind - - javax.validation - validation-api - 1.1.0.Final - @@ -74,31 +74,4 @@ JSON Schema (http://tools.ietf.org/html/draft-zyp-json-schema-03) version 3 gene - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${version.plugin.surefire} - - - com/fasterxml/jackson/module/jsonSchema/failing/*.java - - - - - - org.moditect - moditect-maven-plugin - - - - de.jjohannes - gradle-module-metadata-maven-plugin - - -