IdentifiableJsonSchemaProperty.java

/*
 * Copyright 2018-present the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.data.mongodb.core.schema;

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Range;
import org.springframework.data.mongodb.core.EncryptionAlgorithms;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NullJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * {@link JsonSchemaProperty} implementation.
 *
 * @author Christoph Strobl
 * @author Mark Paluch
 * @since 2.1
 */
public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implements JsonSchemaProperty {

	protected final String identifier;
	protected final T jsonSchemaObjectDelegate;

	/**
	 * Creates a new {@link IdentifiableJsonSchemaProperty} for {@code identifier} and {@code jsonSchemaObject}.
	 *
	 * @param identifier must not be {@literal null}.
	 * @param jsonSchemaObject must not be {@literal null}.
	 */
	IdentifiableJsonSchemaProperty(String identifier, T jsonSchemaObject) {

		Assert.notNull(identifier, "Identifier must not be null");
		Assert.notNull(jsonSchemaObject, "JsonSchemaObject must not be null");

		this.identifier = identifier;
		this.jsonSchemaObjectDelegate = jsonSchemaObject;
	}

	@Override
	public String getIdentifier() {
		return identifier;
	}

	@Override
	public Document toDocument() {
		return new Document(identifier, jsonSchemaObjectDelegate.toDocument());
	}

	@Override
	public Set<Type> getTypes() {
		return jsonSchemaObjectDelegate.getTypes();
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation without a {@code type} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProperty<UntypedJsonSchemaObject> {

		UntypedJsonSchemaProperty(String identifier, UntypedJsonSchemaObject jsonSchemaObject) {
			super(identifier, jsonSchemaObject);
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) {
			return possibleValues(Arrays.asList(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
		}

		/**
		 * @param notMatch must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#notMatch(JsonSchemaObject)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#description(String)
		 */
		@Contract("_ -> new")
		public UntypedJsonSchemaProperty description(String description) {
			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#generateDescription()
		 */
		@Contract("-> new")
		public UntypedJsonSchemaProperty generatedDescription() {
			return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'string'} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class StringJsonSchemaProperty extends IdentifiableJsonSchemaProperty<StringJsonSchemaObject> {

		/**
		 * @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
		 *          {@literal null} nor {@literal empty}.
		 * @param schemaObject must not be {@literal null}.
		 */
		StringJsonSchemaProperty(String identifier, StringJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param length
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#minLength(int)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty minLength(int length) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minLength(length));
		}

		/**
		 * @param length
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#maxLength(int)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty maxLength(int length) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxLength(length));
		}

		/**
		 * @param pattern must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#matching(String)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty matching(String pattern) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.matching(pattern));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty possibleValues(String... possibleValues) {
			return possibleValues(Arrays.asList(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty possibleValues(Collection<String> possibleValues) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
		}

		/**
		 * @param notMatch must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#notMatch(JsonSchemaObject)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#description(String)
		 */
		@Contract("_ -> new")
		public StringJsonSchemaProperty description(String description) {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link StringJsonSchemaProperty}.
		 * @see StringJsonSchemaObject#generateDescription()
		 */
		@Contract("-> new")
		public StringJsonSchemaProperty generatedDescription() {
			return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'object'} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class ObjectJsonSchemaProperty extends IdentifiableJsonSchemaProperty<ObjectJsonSchemaObject> {

		/**
		 * @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
		 *          {@literal null} nor {@literal empty}.
		 * @param schemaObject must not be {@literal null}.
		 */
		ObjectJsonSchemaProperty(String identifier, ObjectJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param range must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty propertiesCount(Range<Integer> range) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.propertiesCount(range));
		}

		/**
		 * @param count must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#minProperties(int)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty minProperties(int count) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minProperties(count));
		}

		/**
		 * @param count must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#maxProperties(int)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty maxProperties(int count) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxProperties(count));
		}

		/**
		 * @param properties must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#required(String...)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty required(String... properties) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.required(properties));
		}

		/**
		 * @param additionalPropertiesAllowed
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#additionalProperties(boolean)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertiesAllowed) {
			return new ObjectJsonSchemaProperty(identifier,
					jsonSchemaObjectDelegate.additionalProperties(additionalPropertiesAllowed));
		}

		/**
		 * @param additionalProperties must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject additionalProperties) {
			return new ObjectJsonSchemaProperty(identifier,
					jsonSchemaObjectDelegate.additionalProperties(additionalProperties));
		}

		/**
		 * @param properties must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.properties(properties));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) {
			return possibleValues(Arrays.asList(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
		}

		/**
		 * @param notMatch must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#notMatch(JsonSchemaObject)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#description(String)
		 */
		@Contract("_ -> new")
		public ObjectJsonSchemaProperty description(String description) {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link ObjectJsonSchemaProperty}.
		 * @see ObjectJsonSchemaObject#generateDescription()
		 */
		@Contract("-> new")
		public ObjectJsonSchemaProperty generatedDescription() {
			return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}

		public List<JsonSchemaProperty> getProperties() {
			return jsonSchemaObjectDelegate.getProperties();
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'number'} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class NumericJsonSchemaProperty extends IdentifiableJsonSchemaProperty<NumericJsonSchemaObject> {

		/**
		 * @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
		 *          {@literal null} nor {@literal empty}.
		 * @param schemaObject must not be {@literal null}.
		 */
		public NumericJsonSchemaProperty(String identifier, NumericJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param value must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#multipleOf
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty multipleOf(Number value) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.multipleOf(value));
		}

		/**
		 * @param range must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#within(Range)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty within(Range<? extends Number> range) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.within(range));
		}

		/**
		 * @param min must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#gt(Number)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty gt(Number min) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gt(min));
		}

		/**
		 * @param min must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#gte(Number)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty gte(Number min) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gte(min));
		}

		/**
		 * @param max must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#lt(Number)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty lt(Number max) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lt(max));
		}

		/**
		 * @param max must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#lte(Number)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty lte(Number max) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lte(max));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty possibleValues(Number... possibleValues) {
			return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues)));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
			return allOf(Arrays.asList(allOf));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty possibleValues(Collection<Number> possibleValues) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
		}

		/**
		 * @param notMatch must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#notMatch(JsonSchemaObject)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#description(String)
		 */
		@Contract("_ -> new")
		public NumericJsonSchemaProperty description(String description) {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see NumericJsonSchemaObject#generateDescription()
		 */
		public NumericJsonSchemaProperty generatedDescription() {
			return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'array'} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class ArrayJsonSchemaProperty extends IdentifiableJsonSchemaProperty<ArrayJsonSchemaObject> {

		/**
		 * @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
		 *          {@literal null} nor {@literal empty}.
		 * @param schemaObject must not be {@literal null}.
		 */
		public ArrayJsonSchemaProperty(String identifier, ArrayJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param uniqueItems
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#uniqueItems(boolean)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.uniqueItems(uniqueItems));
		}

		/**
		 * @param range must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#range(Range)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty range(Range<Integer> range) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range));
		}

		/**
		 * @param count
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#minItems(int)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty minItems(int count) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minItems(count));
		}

		/**
		 * @param count
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#maxItems(int)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty maxItems(int count) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxItems(count));
		}

		/**
		 * @param items must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#items(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty items(JsonSchemaObject... items) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(Arrays.asList(items)));
		}

		/**
		 * @param items must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#items(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty items(Collection<JsonSchemaObject> items) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items));
		}

		/**
		 * @param additionalItemsAllowed
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#additionalItems(boolean)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalItems(additionalItemsAllowed));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) {
			return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues)));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
			return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
			return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
			return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
		}

		/**
		 * @param possibleValues must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#possibleValues(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
		}

		/**
		 * @param allOf must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#allOf(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
		}

		/**
		 * @param anyOf must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#anyOf(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
		}

		/**
		 * @param oneOf must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#oneOf(Collection)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
		}

		/**
		 * @param notMatch must not be {@literal null}.
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#notMatch(JsonSchemaObject)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#description(String)
		 */
		@Contract("_ -> new")
		public ArrayJsonSchemaProperty description(String description) {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link ArrayJsonSchemaProperty}.
		 * @see ArrayJsonSchemaObject#generateDescription()
		 */
		public ArrayJsonSchemaProperty generatedDescription() {
			return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'boolean'} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProperty<BooleanJsonSchemaObject> {

		BooleanJsonSchemaProperty(String identifier, BooleanJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link NumericJsonSchemaProperty}.
		 * @see BooleanJsonSchemaObject#description(String)
		 */
		@Contract("_ -> new")
		public BooleanJsonSchemaProperty description(String description) {
			return new BooleanJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link BooleanJsonSchemaProperty}.
		 * @see BooleanJsonSchemaObject#generateDescription()
		 */
		public BooleanJsonSchemaProperty generatedDescription() {
			return new BooleanJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'null'} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaProperty<NullJsonSchemaObject> {

		NullJsonSchemaProperty(String identifier, NullJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link NullJsonSchemaProperty}.
		 * @see NullJsonSchemaObject#description(String)
		 */
		@Contract("_ -> new")
		public NullJsonSchemaProperty description(String description) {
			return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link NullJsonSchemaProperty}.
		 * @see NullJsonSchemaObject#generateDescription()
		 */
		public NullJsonSchemaProperty generatedDescription() {
			return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'date'} property.
	 *
	 * @author Christoph Strobl
	 * @since 2.1
	 */
	public static class DateJsonSchemaProperty extends IdentifiableJsonSchemaProperty<DateJsonSchemaObject> {

		DateJsonSchemaProperty(String identifier, DateJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link DateJsonSchemaProperty}.
		 * @see DateJsonSchemaProperty#description(String)
		 */
		@Contract("_ -> new")
		public DateJsonSchemaProperty description(String description) {
			return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link DateJsonSchemaProperty}.
		 * @see DateJsonSchemaProperty#generatedDescription()
		 */
		public DateJsonSchemaProperty generatedDescription() {
			return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'timestamp'} property.
	 *
	 * @author Mark Paluch
	 * @since 2.1
	 */
	public static class TimestampJsonSchemaProperty extends IdentifiableJsonSchemaProperty<TimestampJsonSchemaObject> {

		TimestampJsonSchemaProperty(String identifier, TimestampJsonSchemaObject schemaObject) {
			super(identifier, schemaObject);
		}

		/**
		 * @param description must not be {@literal null}.
		 * @return new instance of {@link TimestampJsonSchemaProperty}.
		 * @see TimestampJsonSchemaProperty#description(String)
		 */
		@Contract("_ -> new")
		public TimestampJsonSchemaProperty description(String description) {
			return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
		}

		/**
		 * @return new instance of {@link TimestampJsonSchemaProperty}.
		 * @see TimestampJsonSchemaProperty#generatedDescription()
		 */
		public TimestampJsonSchemaProperty generatedDescription() {
			return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
		}
	}

	/**
	 * Delegating {@link JsonSchemaProperty} implementation having a {@literal required} flag for evaluation during schema
	 * creation process.
	 *
	 * @author Christoph Strobl
	 * @since 2.2
	 */
	public static class RequiredJsonSchemaProperty implements JsonSchemaProperty {

		private final JsonSchemaProperty delegate;
		private final boolean required;

		RequiredJsonSchemaProperty(JsonSchemaProperty delegate, boolean required) {

			this.delegate = delegate;
			this.required = required;
		}

		@Override
		public String getIdentifier() {
			return delegate.getIdentifier();
		}

		@Override
		public Set<Type> getTypes() {
			return delegate.getTypes();
		}

		@Override
		public Document toDocument() {
			return delegate.toDocument();
		}

		@Override
		public boolean isRequired() {
			return required;
		}
	}

	/**
	 * {@link JsonSchemaProperty} implementation for encrypted fields.
	 *
	 * @author Christoph Strobl
	 * @since 2.2
	 */
	public static class EncryptedJsonSchemaProperty implements JsonSchemaProperty {

		private final JsonSchemaProperty targetProperty;
		private final @Nullable String algorithm;
		private final @Nullable Object keyId;
		private final @Nullable List<?> keyIds;

		/**
		 * Create new instance of {@link EncryptedJsonSchemaProperty} wrapping the given {@link JsonSchemaProperty target}.
		 *
		 * @param target must not be {@literal null}.
		 */
		public EncryptedJsonSchemaProperty(JsonSchemaProperty target) {
			this(target, null, null, null);
		}

		private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable Object keyId,
				@Nullable List<?> keyIds) {

			Assert.notNull(target, "Target must not be null");
			this.targetProperty = target;
			this.algorithm = algorithm;
			this.keyId = keyId;
			this.keyIds = keyIds;
		}

		/**
		 * Create new instance of {@link EncryptedJsonSchemaProperty} wrapping the given {@link JsonSchemaProperty target}.
		 *
		 * @param target must not be {@literal null}.
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 */
		public static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty target) {
			return new EncryptedJsonSchemaProperty(target);
		}

		/**
		 * Create new instance of {@link EncryptedJsonSchemaProperty} with {@literal Range} encryption, wrapping the given
		 * {@link JsonSchemaProperty target}.
		 *
		 * @param target must not be {@literal null}.
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 * @since 4.5
		 */
		public static EncryptedJsonSchemaProperty rangeEncrypted(JsonSchemaProperty target) {
			return new EncryptedJsonSchemaProperty(target).algorithm(EncryptionAlgorithms.RANGE);
		}

		/**
		 * Use {@literal AEAD_AES_256_CBC_HMAC_SHA_512-Random} algorithm.
		 *
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 */
		@Contract("-> new")
		public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() {
			return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
		}

		/**
		 * Use {@literal AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic} algorithm.
		 *
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 */
		@Contract("-> new")
		public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() {
			return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
		}

		/**
		 * Use the given algorithm identified via its name.
		 *
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 */
		@Contract("_ -> new")
		public EncryptedJsonSchemaProperty algorithm(String algorithm) {
			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, keyIds);
		}

		/**
		 * @param keyId must not be {@literal null}.
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 */
		@Contract("_ -> new")
		public EncryptedJsonSchemaProperty keyId(String keyId) {
			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null);
		}

		/**
		 * @param keyId must not be {@literal null}.
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 * @since 4.5
		 */
		public EncryptedJsonSchemaProperty keyId(Object keyId) {
			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null);
		}

		/**
		 * @param keyId must not be {@literal null}.
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 */
		@Contract("_ -> new")
		public EncryptedJsonSchemaProperty keys(UUID... keyId) {
			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId));
		}

		/**
		 * @param keyId must not be {@literal null}.
		 * @return new instance of {@link EncryptedJsonSchemaProperty}.
		 */
		@Contract("_ -> new")
		public EncryptedJsonSchemaProperty keys(Object... keyId) {
			return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId));
		}

		@Override
		public Document toDocument() {

			Document doc = targetProperty.toDocument();
			Document propertySpecification = doc.get(targetProperty.getIdentifier(), Document.class);

			Document enc = new Document();

			if (!ObjectUtils.isEmpty(keyId)) {
				enc.append("keyId", keyId);
			} else if (!ObjectUtils.isEmpty(keyIds)) {
				enc.append("keyId", keyIds);
			}

			Type type = extractPropertyType(propertySpecification);
			if (type != null) {

				propertySpecification.remove(type.representation());
				enc.append("bsonType", type.toBsonType().value()); // TODO: no samples with type -> is it bson type all the way?
			}

			if (StringUtils.hasText(algorithm)) {
				enc.append("algorithm", algorithm);
			}

			propertySpecification.append("encrypt", enc);

			return doc;
		}

		@Override
		public String getIdentifier() {
			return targetProperty.getIdentifier();
		}

		@Override
		public Set<Type> getTypes() {
			return targetProperty.getTypes();
		}


		private @Nullable Type extractPropertyType(Document source) {

			if (source.containsKey("type")) {
				return Type.of(source.get("type", String.class));
			}
			if (source.containsKey("bsonType")) {
				return Type.of(source.get("bsonType", String.class));
			}

			return null;
		}

		public @Nullable Object getKeyId() {
			if (keyId != null) {
				return keyId;
			}
			if (keyIds != null && keyIds.size() == 1) {
				return keyIds.iterator().next();
			}
			return null;
		}
	}

	/**
	 * {@link JsonSchemaProperty} implementation typically wrapping an {@link EncryptedJsonSchemaProperty encrypted
	 * property} to mark it as queryable.
	 *
	 * @author Christoph Strobl
	 * @since 4.5
	 */
	public static class QueryableJsonSchemaProperty implements JsonSchemaProperty {

		private final JsonSchemaProperty targetProperty;
		private final QueryCharacteristics characteristics;

		public QueryableJsonSchemaProperty(JsonSchemaProperty target, QueryCharacteristics characteristics) {
			this.targetProperty = target;
			this.characteristics = characteristics;
		}

		@Override
		public Document toDocument() {

			Document doc = targetProperty.toDocument();
			Document propertySpecification = doc.get(targetProperty.getIdentifier(), Document.class);

			if (propertySpecification.containsKey("encrypt")) {
				Document encrypt = propertySpecification.get("encrypt", Document.class);
				List<Document> queries = characteristics.getCharacteristics().stream().map(QueryCharacteristic::toDocument)
						.toList();
				encrypt.append("queries", queries);
			}

			return doc;
		}

		@Override
		public String getIdentifier() {
			return targetProperty.getIdentifier();
		}

		@Override
		public Set<Type> getTypes() {
			return targetProperty.getTypes();
		}

		boolean isEncrypted() {
			return targetProperty instanceof EncryptedJsonSchemaProperty;
		}

		public JsonSchemaProperty getTargetProperty() {
			return targetProperty;
		}

		public QueryCharacteristics getCharacteristics() {
			return characteristics;
		}
	}
}