T getPropertyValue(VaultPersistentProperty property) {
+
+ Object value = source.get(property);
+
+ if (value == null) {
+ return null;
+ }
+
+ return readValue(value, property.getTypeInformation());
+ }
+ }
+}
diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java
new file mode 100644
index 00000000..7a338007
--- /dev/null
+++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2017 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
+ *
+ * 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 org.springframework.vault.repository.convert;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.vault.support.VaultResponse;
+
+/**
+ * Vault database exchange object containing data before/after it's exchanged with Vault.
+ * A {@link SecretDocument} is basically an object with an {@code id} and a body
+ * represented as {@link Map} of {@link String} and {@link Object}. It can be created
+ * {@link #from(String, VaultResponse) from} an Id and {@link VaultResponse}.
+ *
+ * A secret document can hold simple properties, {@link java.util.Collection list}
+ * properties and nested objects as {@link Map}s.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+@EqualsAndHashCode
+@ToString
+public class SecretDocument {
+
+ private @Nullable String id;
+
+ private final Map body;
+
+ /**
+ * Create a new, empty {@link SecretDocument}.
+ */
+ public SecretDocument() {
+ this(null, new LinkedHashMap<>());
+ }
+
+ /**
+ * Create a new {@link SecretDocument} given a {@link Map body map}.
+ * @param body must not be {@literal null}.
+ */
+ public SecretDocument(Map body) {
+ this(null, body);
+ }
+
+ /**
+ * Create a new {@link SecretDocument} given an {@code id} and {@link Map body map}.
+ * @param id may be {@literal null}.
+ * @param body must not be {@literal null}.
+ */
+ public SecretDocument(@Nullable String id, Map body) {
+
+ Assert.notNull(body, "Body must not be null");
+
+ this.id = id;
+ this.body = body;
+ }
+
+ public SecretDocument(String id) {
+ this(id, new LinkedHashMap<>());
+ }
+
+ /**
+ * Factory method to create a {@link SecretDocument} from an {@code id} and
+ * {@link VaultResponse}.
+ *
+ * @param id must not be {@literal null}.
+ * @param vaultResponse must not be {@literal null}.
+ * @return the {@link SecretDocument}.
+ */
+ @SuppressWarnings("ConstantConditions")
+ public static SecretDocument from(@Nullable String id, VaultResponse vaultResponse) {
+ return new SecretDocument(id, vaultResponse.getData());
+ }
+
+ /**
+ * @return the Id or {@literal null} if the Id is not set.
+ */
+ @Nullable
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Set the Id.
+ *
+ * @param id may be {@literal null}.
+ */
+ public void setId(@Nullable String id) {
+ this.id = id;
+ }
+
+ /**
+ * @return the body of this {@link SecretDocument}
+ */
+ public Map getBody() {
+ return body;
+ }
+
+ /**
+ * Retrieve a value from the secret document by its {@code key}.
+ *
+ * @param key must not be {@literal null}.
+ * @return the value or {@literal null}, if the value is not present.
+ */
+ @Nullable
+ public Object get(String key) {
+ return body.get(key);
+ }
+
+ /**
+ * Set a value in the secret document.
+ *
+ * @param key must not be {@literal null}.
+ * @param value must not be {@literal null}.
+ */
+ public void put(String key, Object value) {
+ this.body.put(key, value);
+ }
+}
diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocumentAccessor.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocumentAccessor.java
new file mode 100644
index 00000000..86b26b7c
--- /dev/null
+++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocumentAccessor.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2017 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
+ *
+ * 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 org.springframework.vault.repository.convert;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.vault.repository.mapping.VaultPersistentProperty;
+
+/**
+ * Wrapper value object for a {@link SecretDocument} to be able to access raw values by
+ * {@link VaultPersistentProperty} references. The accessors will transparently resolve
+ * nested document values that a {@link VaultPersistentProperty} might refer to through a
+ * path expression in field names.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+class SecretDocumentAccessor {
+
+ private final SecretDocument document;
+
+ private final Map body;
+
+ /**
+ * Creates a new {@link SecretDocumentAccessor} for the given {@link SecretDocument}.
+ *
+ * @param document must be a {@link SecretDocument} effectively, must not be
+ * {@literal null}.
+ */
+ SecretDocumentAccessor(SecretDocument document) {
+
+ Assert.notNull(document, "SecretDocument must not be null!");
+
+ this.document = document;
+ this.body = document.getBody();
+ }
+
+ /**
+ * Creates a new {@link SecretDocumentAccessor} for the given {@link SecretDocument}
+ * and {@link Map body}.
+ *
+ * @param document must be a {@link SecretDocument} effectively, must not be
+ * {@literal null}
+ * @param body must not be {@literal null}.
+ */
+ private SecretDocumentAccessor(SecretDocument document, Map body) {
+
+ Assert.notNull(document, "SecretDocument must not be null!");
+ Assert.notNull(body, "Body must not be null!");
+
+ this.document = document;
+ this.body = body;
+ }
+
+ /**
+ * Puts the given value into the backing {@link SecretDocument} based on the
+ * coordinates defined through the given {@link VaultPersistentProperty}. By default
+ * this will be the plain field name. But field names might also consist of path
+ * traversals so we might need to create intermediate {@link Map}s.
+ *
+ * @param prop must not be {@literal null}.
+ * @param value
+ */
+ void put(VaultPersistentProperty prop, @Nullable Object value) {
+
+ Assert.notNull(prop, "VaultPersistentProperty must not be null!");
+ String fieldName = prop.getName();
+
+ if (prop.isIdProperty()) {
+ this.document.setId((String) value);
+ return;
+ }
+
+ if (!fieldName.contains(".")) {
+ this.body.put(fieldName, value);
+ return;
+ }
+
+ Iterator parts = Arrays.asList(fieldName.split("\\.")).iterator();
+ Map document = this.body;
+
+ while (parts.hasNext()) {
+
+ String part = parts.next();
+
+ if (parts.hasNext()) {
+ document = getOrCreateNestedDocument(part, document);
+ }
+ else {
+ document.put(fieldName, value);
+ }
+ }
+ }
+
+ /**
+ * Returns the value the given {@link VaultPersistentProperty} refers to. By default
+ * this will be a direct field but the method will also transparently resolve nested
+ * values the {@link VaultPersistentProperty} might refer to through a path expression
+ * in the field name metadata.
+ *
+ * @param property must not be {@literal null}.
+ * @return
+ */
+ @Nullable
+ Object get(VaultPersistentProperty property) {
+
+ String fieldName = property.getName();
+
+ if (property.isIdProperty()) {
+ return this.document.getId();
+ }
+
+ if (!fieldName.contains(".")) {
+ return this.body.get(fieldName);
+ }
+
+ Iterator parts = Arrays.asList(fieldName.split("\\.")).iterator();
+ Map source = this.body;
+ Object result = null;
+
+ while (source != null && parts.hasNext()) {
+
+ result = source.get(parts.next());
+
+ if (parts.hasNext()) {
+ source = getAsMap(result);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns whether the underlying {@link SecretDocument} has a value ({@literal null}
+ * or non-{@literal null}) for the given {@link VaultPersistentProperty}.
+ *
+ * @param property must not be {@literal null}.
+ * @return
+ */
+ boolean hasValue(VaultPersistentProperty property) {
+
+ Assert.notNull(property, "Property must not be null!");
+
+ if (property.isIdProperty()) {
+ return StringUtils.hasText(this.document.getId());
+ }
+
+ String fieldName = property.getName();
+
+ if (!fieldName.contains(".")) {
+ return this.body.containsKey(fieldName);
+ }
+
+ String[] parts = fieldName.split("\\.");
+ Map source = this.body;
+
+ Object result = null;
+
+ for (int i = 1; i < parts.length; i++) {
+
+ result = source.get(parts[i - 1]);
+ source = getAsMap(result);
+
+ if (source == null) {
+ return false;
+ }
+ }
+
+ return source.containsKey(parts[parts.length - 1]);
+ }
+
+ /**
+ * Returns the given source object as map, i.e. maps as is or {@literal null}
+ * otherwise.
+ *
+ * @param source can be {@literal null}.
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private static Map getAsMap(Object source) {
+
+ if (source instanceof Map) {
+ return (Map) source;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link Map} which either already exists in the given source under the
+ * given key, or creates a new nested one, registers it with the source and returns
+ * it.
+ *
+ * @param key must not be {@literal null} or empty.
+ * @param source must not be {@literal null}.
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ private static Map getOrCreateNestedDocument(String key,
+ Map source) {
+
+ Object existing = source.get(key);
+
+ if (existing instanceof Map) {
+ return (Map) existing;
+ }
+
+ Map nested = new LinkedHashMap<>();
+ source.put(key, nested);
+
+ return nested;
+ }
+
+ public Map getBody() {
+ return body;
+ }
+
+ public void setId(String id) {
+ this.document.setId(id);
+ }
+
+ /**
+ * Obtains a nested {@link SecretDocumentAccessor} for a
+ * {@link VaultPersistentProperty}. Nested accessors allows mapping of structured
+ * hierarchies and represent accessors to nested maps.
+ *
+ * @param property must not be {@literal null}.
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ public SecretDocumentAccessor writeNested(VaultPersistentProperty property) {
+
+ Map body = (Map) get(property);
+
+ if (body == null) {
+ body = new LinkedHashMap<>();
+ put(property, body);
+ }
+
+ return new SecretDocumentAccessor(document, body);
+ }
+}
diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultConverter.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultConverter.java
new file mode 100644
index 00000000..9d71aa5c
--- /dev/null
+++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultConverter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 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
+ *
+ * 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 org.springframework.vault.repository.convert;
+
+import org.springframework.data.convert.EntityConverter;
+import org.springframework.vault.repository.mapping.VaultPersistentEntity;
+import org.springframework.vault.repository.mapping.VaultPersistentProperty;
+
+/**
+ * Central Vault-specific converter interface.
+ *
+ * @since 2.0
+ */
+public interface VaultConverter
+ extends
+ EntityConverter, VaultPersistentProperty, Object, SecretDocument> {
+}
diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultCustomConversions.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultCustomConversions.java
new file mode 100644
index 00000000..8b762c2a
--- /dev/null
+++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultCustomConversions.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 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
+ *
+ * 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 org.springframework.vault.repository.convert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.converter.GenericConverter;
+import org.springframework.data.convert.JodaTimeConverters;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.vault.repository.mapping.VaultSimpleTypes;
+
+/**
+ * Value object to capture custom conversion. {@link VaultCustomConversions} also act as
+ * factory for {@link org.springframework.data.mapping.model.SimpleTypeHolder}
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ * @see org.springframework.data.convert.CustomConversions
+ * @see org.springframework.data.mapping.model.SimpleTypeHolder
+ * @see VaultSimpleTypes
+ */
+public class VaultCustomConversions extends
+ org.springframework.data.convert.CustomConversions {
+
+ private static final StoreConversions STORE_CONVERSIONS;
+ private static final List