diff --git a/data4k/README.md b/data4k/README.md
new file mode 100644
index 0000000..619b666
--- /dev/null
+++ b/data4k/README.md
@@ -0,0 +1,56 @@
+# Data4k
+
+
+[![.github/workflows/build.yaml](https://github.com/fork-handles/forkhandles/actions/workflows/build.yaml/badge.svg)](https://github.com/fork-handles/forkhandles/actions/workflows/build.yaml)
+
+
+
+
+Library to make working with Data-Oriented programming in Kotlin easier, to extract values from dynamic data structures such as Maps.
+
+## Installation
+
+In Gradle, install the ForkHandles BOM and then this module in the dependency block:
+
+```kotlin
+implementation(platform("dev.forkhandles:forkhandles-bom:X.Y.Z"))
+implementation("dev.forkhandles:data4k")
+```
+
+## Usage
+
+The library defines a DataContainer class and implementations for:
+
+- Map
+- Jackson JSON Node
+
+To extract data from the underlying data, define wrappers which provides access via delegated-properties:
+
+```kotlin
+class MapBacked(propertySet: Map) : MapDataContainer(propertySet) {
+ val stringField by field()
+ val listSubClassField by list(::SubMap)
+ val objectField by obj(::SubMap)
+}
+
+class SubMap(propertySet: Map) : MapDataContainer(propertySet) {
+ val stringField by field()
+}
+
+val input = MapBacked(
+ mapOf(
+ "stringField" to "string",
+ "listSubClassField" to listOf(
+ mapOf("stringField" to "string1"),
+ mapOf("stringField" to "string2"),
+ ),
+ "listStringsField" to listOf("string1", "string2"),
+ "objectField" to mapOf(
+ "stringField" to "string"
+ )
+ )
+)
+
+// then just get the values from the underlying data using the type system. Errors will be thrown for missing/invalid properties
+val data: String = input.objectField.stringField
+```
diff --git a/data4k/build.gradle b/data4k/build.gradle
new file mode 100644
index 0000000..6655030
--- /dev/null
+++ b/data4k/build.gradle
@@ -0,0 +1,8 @@
+description = 'ForkHandles data-oriented programming library'
+
+dependencies {
+ api("org.jetbrains.kotlin:kotlin-reflect:_")
+ compileOnly("com.fasterxml.jackson.core:jackson-databind:_")
+ testImplementation("com.fasterxml.jackson.core:jackson-databind:_")
+ testImplementation("io.strikt:strikt-jvm:_")
+}
diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt
new file mode 100644
index 0000000..b8dbe6a
--- /dev/null
+++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt
@@ -0,0 +1,23 @@
+package dev.forkhandles.data
+
+/**
+ * Superclass for all container implementations. Defines the delegate property classes to extract data from the
+ * underlying data structure.
+ */
+@Suppress("UNCHECKED_CAST", "ClassName")
+abstract class DataContainer(
+ protected val data: CONTENT,
+ existsFn: (CONTENT, String) -> Boolean,
+ getFn: (CONTENT, String) -> Any?
+) {
+ private val exists: DataContainer.(String) -> Boolean = { existsFn(data, it) }
+ private val get: DataContainer.(String) -> Any? = { getFn(data, it) }
+
+ inner class field : DataProperty, OUT>(exists, get)
+
+ inner class list(mapFn: (IN) -> OUT) :
+ DataProperty, List>(exists, { (get(it) as List).map(mapFn) })
+
+ inner class obj>(mapFn: (CONTENT) -> OUT) :
+ DataProperty, OUT>(exists, { mapFn(get(it) as CONTENT) })
+}
diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt
similarity index 93%
rename from lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt
rename to data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt
index 403b3b1..7a98ba0 100644
--- a/lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt
+++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt
@@ -1,10 +1,10 @@
-package dev.forkhandles.lens
+package dev.forkhandles.data
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.jvmErasure
-abstract class LensProp(
+abstract class DataProperty(
private val existsFn: IN.(String) -> Boolean,
private val getFn: IN.(String) -> Any?
) : ReadOnlyProperty {
diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt
new file mode 100644
index 0000000..8de3e22
--- /dev/null
+++ b/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt
@@ -0,0 +1,30 @@
+package dev.forkhandles.data
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.*
+
+/**
+ * Jackson JsonNode-based implementation of the DataContainer
+ */
+abstract class JacksonDataContainer(input: JsonNode) :
+ DataContainer(
+ input,
+ { content, it -> content.has(it) },
+ { content, it -> content[it]?.let(Companion::nodeToValue) }
+ ) {
+
+ companion object {
+ private fun nodeToValue(input: JsonNode): Any? = when (input) {
+ is BooleanNode -> input.booleanValue()
+ is IntNode -> input.intValue()
+ is LongNode -> input.longValue()
+ is DecimalNode -> input.decimalValue()
+ is DoubleNode -> input.doubleValue()
+ is TextNode -> input.textValue()
+ is ArrayNode -> input.map(::nodeToValue)
+ is ObjectNode -> input
+ is NullNode -> null
+ else -> error("Invalid node type $input")
+ }
+ }
+}
diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt
new file mode 100644
index 0000000..6b1759f
--- /dev/null
+++ b/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt
@@ -0,0 +1,11 @@
+package dev.forkhandles.data
+
+import kotlin.collections.Map
+
+/**
+ * Map-based implementation of the DataContainer
+ */
+abstract class MapDataContainer(input: Map) :
+ DataContainer