diff --git a/Cargo.toml b/Cargo.toml
index cc641171e49f480b7cc21852c559ebe469d59e5a..d651218d033215f213724d302e3ea6c888758ec2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ log = "^0.4.11"
 syslog = "^5.0.0"
 xdg = "^2.2.0"
 serde_json = "^1.0.64"
+jq-rs = "^0.4.1"
 
 [profile.release]
 opt-level = 'z'
diff --git a/README.md b/README.md
index 8755bc8e4d6aa8c301f25648f9a346db11fad1fa..fe9dcc5cc582bbbd1ec9dc35426a2125c8ffacf3 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,17 @@ from the API up to date. It handles the following data:
  * User access tokens (using corresponding refresh tokens, if available)
  * NSS data
 
+## Installation
+
+### Building from source
+
+To build from source, development headers for `libjq` and `libonig` are
+required. On Debian, install them with:
+
+```shell
+sudo apt install libjq-dev libonig-dev
+```
+
 ## Credits
 
 Special thanks to mirabilos in his position as Senior Unix System Development
diff --git a/etc/nss_pam_oidc.example.toml b/etc/nss_pam_oidc.example.toml
index f965805f1f563156140f193a400ce454c752fdd1..ac4e2035abe3f1d5d2dee3dc12557c64ad9e3338 100644
--- a/etc/nss_pam_oidc.example.toml
+++ b/etc/nss_pam_oidc.example.toml
@@ -9,13 +9,17 @@ client_secret = ""
 [nss]
 client_id = "z8Oz0tG56QRo9QEPUZTs5Eda410FMiJtYxlInxKE"
 client_secret = ""
-passwd_url = "https://ticdesk-dev.teckids.org/app/nis/api/passwd/"
 
-[nss.maps.passwd]
-name = { type = "rename", value = "username" }
-passwd = { type = "static", value = "x" }
-# uid left unchanged
-gid = { type = "rename", value = "primary_gid" }
-gecos = { type = "static", value = "Foo" }
-dir = { type = "rename", value = "home_directory" }
-shell = { type = "rename", value = "login_shell" }
+urls.passwd = "https://ticdesk-dev.teckids.org/app/nis/api/passwd/"
+
+maps.passwd = """
+    {
+        name: .username,
+        passwd: "x",
+        uid: .uid,
+        gid: .primary_gid,
+        gecos: "Foo",
+        dir: .home_directory,
+        shell: .login_shell
+    }
+"""
diff --git a/src/nss.rs b/src/nss.rs
index e0d5acaed86375cf38b0266b796dd0ed477a638f..66ab75f44d94f3d8f08f4e4f54f78ae61d9b12a8 100644
--- a/src/nss.rs
+++ b/src/nss.rs
@@ -23,11 +23,8 @@ use crate::cache::get_cache;
 
 use crate::logging::setup_log;
 
-use crate::oauth::get_data;
-use std::collections::HashMap;
-use serde_json::value::Value;
-use std::fmt;
-use std::convert::TryInto;
+use crate::oauth::get_data_jq;
+use serde::{Serialize, Deserialize};
 
 use libc::{getpwuid, geteuid};
 use std::ffi::CStr;
@@ -35,6 +32,19 @@ use std::ffi::CStr;
 use libnss::interop::Response;
 use libnss::passwd::{PasswdHooks, Passwd};
 
+#[derive(Serialize, Deserialize)]
+#[serde(remote = "Passwd")]
+struct PasswdDef {
+    name: String,
+    passwd: String,
+    uid: libc::uid_t,
+    gid: libc::gid_t,
+    gecos: String,
+    dir: String,
+    shell: String
+}
+#[derive(Deserialize)] struct PasswdHelper(#[serde(with = "PasswdDef")] Passwd);
+
 fn nss_hook_prepare() -> Config {
     let conf = get_config(None);
 
@@ -57,75 +67,6 @@ fn get_current_user() -> String {
     euser.to_str().ok().unwrap().to_string()
 }
 
-// FIXME Provide more specific error types and messages
-#[derive(Debug, Clone)]
-struct TransformMappingError {
-    msg: String,
-    field: String
-}
-impl fmt::Display for TransformMappingError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "Invalid mapping configuration: field={} - {}", self.field, self.msg)
-    }
-}
-
-fn transform_ent(conf: &Config, row: &mut HashMap<String, Value>, map_name: &str) -> Result<(), TransformMappingError> {
-    let mapping: HashMap<String, HashMap<String, Value>> = get_optional(&conf, &("nss.maps.".to_string() + map_name)).unwrap_or_default();
-
-    for (field, rule) in &mapping {
-        let type_: String = rule.get("type")
-            .ok_or(TransformMappingError { field: field.to_string(), msg: "No type".to_string() })?
-            .as_str().unwrap().to_string();
-        let value: &Value = rule.get("value")
-            .ok_or(TransformMappingError { field: field.to_string(), msg: "No value".to_string() })?;
-
-        if type_ == "static" {
-            row.insert(field.to_string(), value.clone());
-        } else if type_ == "rename" {
-            let old_value: Value = row.remove(&value.as_str().unwrap().to_string())
-                .ok_or(TransformMappingError { field: field.to_string(), msg: "No value to rename".to_string() })?;
-            row.insert(field.to_string(), old_value);
-        } else {
-            return Err(TransformMappingError { field: field.to_string(), msg: (&("Unknown type ".to_string() + &type_)).to_string() });
-        };
-    }
-
-    Ok(())
-}
-
-fn map_to_passwd(conf: &Config, row: &mut HashMap<String, Value>) -> Result<Passwd, TransformMappingError> {
-    transform_ent(&conf, row, "passwd")?;
-    Ok(Passwd {
-        name: row.get("name")
-            .ok_or(TransformMappingError { field: "name".to_string(), msg: "No value in JSON data".to_string() })?
-            .as_str().unwrap().to_string(),
-        passwd: row.get("passwd")
-            .ok_or(TransformMappingError { field: "passwd".to_string(), msg: "No value in JSON data".to_string() })?
-            .as_str().unwrap().to_string(),
-        uid: row.get("uid")
-            .ok_or(TransformMappingError { field: "uid".to_string(), msg: "No value in JSON data".to_string() })?
-            .as_u64()
-            .ok_or(TransformMappingError { field: "uid".to_string(), msg: "Invalid integer".to_string() })?
-            .try_into()
-            .or(Err(TransformMappingError { field: "uid".to_string(), msg: "Overflow converting to u32".to_string() }))?,
-        gid: row.get("gid")
-            .ok_or(TransformMappingError { field: "gid".to_string(), msg: "No value in JSON data".to_string() })?
-            .as_u64()
-            .ok_or(TransformMappingError { field: "gid".to_string(), msg: "Invalid integer".to_string() })?
-            .try_into()
-            .or(Err(TransformMappingError { field: "gid".to_string(), msg: "Overflow converting to u32".to_string() }))?,
-        gecos: row.get("gecos")
-            .ok_or(TransformMappingError { field: "gecos".to_string(), msg: "No value in JSON data".to_string() })?
-            .as_str().unwrap().to_string(),
-        dir: row.get("dir")
-            .ok_or(TransformMappingError { field: "dir".to_string(), msg: "No value in JSON data".to_string() })?
-            .as_str().unwrap().to_string(),
-        shell: row.get("shell")
-            .ok_or(TransformMappingError { field: "shell".to_string(), msg: "No value in JSON data".to_string() })?
-            .as_str().unwrap().to_string(),
-    })
-}
-
 struct OidcPasswd;
 
 impl PasswdHooks for OidcPasswd {
@@ -143,20 +84,14 @@ impl PasswdHooks for OidcPasswd {
             }
         };
 
-        let mut data: Vec<HashMap<String, Value>> = match get_data(&conf, "nss", "passwd", token, "") {
+        let data: Vec<PasswdHelper> = match get_data_jq(&conf, "nss", "passwd", token, true) {
             Ok(d) => d,
-            Err(_) => return Response::Unavail
+            Err(_) => {
+                error!("Could not load JSON data for passwd");
+                return Response::Unavail;
+            }
         };
-
-        let mut passwd_vec: Vec<Passwd> = Vec::new();
-        for row in &mut data {
-            match map_to_passwd(&conf, row) {
-                Ok(p) => passwd_vec.push(p),
-                Err(e) => error!("Error converting JSON to passwd entry: {}", e)
-            };
-        }
-
-        Response::Success(passwd_vec)
+        Response::Success(data.into_iter().map(|p| p.0).collect())
     }
 
     fn get_entry_by_uid(uid: libc::uid_t) -> Response<Passwd> {
diff --git a/src/oauth.rs b/src/oauth.rs
index 71e8c04f18f21f4638da564935b950a200e82bda..385d41f983b5603541727aabba6e8c9e8d0f3580 100644
--- a/src/oauth.rs
+++ b/src/oauth.rs
@@ -37,29 +37,32 @@ use oauth2::basic::{
 };
 use oauth2::reqwest::http_client;
 
+use std::error;
+
 use serde::Deserialize;
 use reqwest;
 
-fn full_key(prefix: &str, key: &str) -> String {
-    let parts = vec![prefix.to_string(), key.to_string()];
-    let full_key = parts.join(".");
-    return full_key;
+use serde_json;
+use jq_rs;
+
+fn full_key(parts: Vec<&str>) -> String {
+    parts.join(".")
 }
 
 fn get_client<E: Copy>(conf: Config, prefix: &str, error_value: E) -> Result<BasicClient, E> {
-    let client_id = ClientId::new(get_or_error(&conf, &full_key(prefix, "client_id"), error_value)?);
-    let client_secret = match get_optional(&conf, &full_key(prefix, "client_secret")) {
+    let client_id = ClientId::new(get_or_error(&conf, &full_key(vec![prefix, "client_id"]), error_value)?);
+    let client_secret = match get_optional(&conf, &full_key(vec![prefix, "client_secret"])) {
         Some(v) => Some(ClientSecret::new(v)),
         None => None,
     };
-    let auth_url = match AuthUrl::new(get_or_error(&conf, &full_key(prefix, "auth_url"), error_value)?) {
+    let auth_url = match AuthUrl::new(get_or_error(&conf, &full_key(vec![prefix, "auth_url"]), error_value)?) {
         Ok(u) => u,
         _ => {
             error!("Could not parse authorization URL");
             return Err(error_value);
         },
     };
-    let token_url = match get_optional(&conf, &full_key(prefix, "token_url")) {
+    let token_url = match get_optional(&conf, &full_key(vec![prefix, "token_url"])) {
         Some(v) => match TokenUrl::new(v) {
             Ok(u) => Some(u),
             Err(_) => {
@@ -75,7 +78,7 @@ fn get_client<E: Copy>(conf: Config, prefix: &str, error_value: E) -> Result<Bas
 }
 
 pub fn get_access_token_client<E: Copy>(conf: Config, prefix: &str, error_value: E, unauth_value: E) -> Result<BasicTokenResponse, E> {
-    let scopes: Vec<String> = match get_optional(&conf, &full_key(prefix, "scopes")) {
+    let scopes: Vec<String> = match get_optional(&conf, &full_key(vec![prefix, "scopes"])) {
         Some(v) => v,
         None => vec![]
     };
@@ -103,7 +106,7 @@ pub fn get_access_token_client<E: Copy>(conf: Config, prefix: &str, error_value:
 }
 
 pub fn get_access_token_password<E: Copy>(conf: Config, prefix: &str, username: String, password: String, error_value: E, unauth_value: E) -> Result<BasicTokenResponse, E> {
-    let scopes: Vec<String> = match get_optional(&conf, &full_key(prefix, "scopes")) {
+    let scopes: Vec<String> = match get_optional(&conf, &full_key(vec![prefix, "scopes"])) {
         Some(v) => v,
         None => vec![]
     };
@@ -133,31 +136,32 @@ pub fn get_access_token_password<E: Copy>(conf: Config, prefix: &str, username:
         }
 }
 
-pub fn get_data<T: for<'de> Deserialize<'de>, E: Copy>(conf: &Config, prefix: &str, endpoint: &str, token: &BasicTokenResponse, error_value: E) -> Result<T, E> {
+fn get_data(conf: &Config, prefix: &str, endpoint: &str, token: &BasicTokenResponse) -> Result<String, Box<dyn error::Error>> {
     let access_token = token.access_token().secret();
-    let endpoint_url: String = get_or_error(&conf, &full_key(prefix, &(endpoint.to_string() + "_url")), error_value)?;
+    let endpoint_url: String = get_or_error(&conf, &full_key(vec![prefix, "urls", endpoint]), "")?;
 
-    info!("Loading JSON data from {}", endpoint_url);
+    debug!("Loading text data from {}", endpoint_url);
     let client = reqwest::blocking::Client::new();
-    let res = match client
+    Ok(client
         .get(&endpoint_url)
         .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token))
-        .send() {
-            Ok(r) => r,
-            Err(e) => {
-                error!("Could not complete HTTP request: {}", e);
-                return Err(error_value);
-            }
-        };
+        .send()?
+        .text()?)
+}
 
-    let data = match res.json() {
-        Ok(d) => d,
-        Err(e) => {
-            error!("Could not parse JSON response: {}", e);
-            return Err(error_value);
-        }
+pub fn get_data_jq<T: for<'de> Deserialize<'de>>(conf: &Config, prefix: &str, endpoint: &str, token: &BasicTokenResponse, multi: bool) -> Result<T, Box<dyn error::Error>> {
+    let res: Option<String> = get_optional(&conf, &full_key(vec![prefix, "maps", endpoint]));
+    let jq_code = match res {
+        Some(s) => match multi {
+            true => "map(".to_string() + &s + ")",
+            false => s
+        },
+        None => ".".to_string()
     };
+    let mut jq_prog = jq_rs::compile(&jq_code)?;
+
+    let data_raw = get_data(&conf, prefix, endpoint, token)?;
+    let data_trans = jq_prog.run(&data_raw)?;
 
-    debug!("Successfully loaded JSON data from {}", endpoint_url);
-    return Ok(data);
+    Ok(serde_json::from_str(&data_trans)?)
 }