diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt
index e3d060a1fa..d9cdd9a0ce 100644
--- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt
+++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt
@@ -1,9 +1,6 @@
 package net.corda.jackson
 
-import com.fasterxml.jackson.core.JsonGenerator
-import com.fasterxml.jackson.core.JsonParseException
-import com.fasterxml.jackson.core.JsonParser
-import com.fasterxml.jackson.core.JsonToken
+import com.fasterxml.jackson.core.*
 import com.fasterxml.jackson.databind.*
 import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
 import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
@@ -30,6 +27,7 @@ import java.util.*
  * Note that Jackson can also be used to serialise/deserialise other formats such as Yaml and XML.
  */
 object JacksonSupport {
+    // TODO: This API could use some tidying up - there should really only need to be one kind of mapper.
     // If you change this API please update the docs in the docsite (json.rst)
 
     interface PartyObjectMapper {
@@ -37,15 +35,15 @@ object JacksonSupport {
         fun partyFromKey(owningKey: CompositeKey): Party?
     }
 
-    class RpcObjectMapper(val rpc: CordaRPCOps) : PartyObjectMapper, ObjectMapper() {
+    class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
         override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
         override fun partyFromKey(owningKey: CompositeKey): Party? = rpc.partyFromKey(owningKey)
     }
-    class IdentityObjectMapper(val identityService: IdentityService) : PartyObjectMapper, ObjectMapper(){
+    class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
         override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
         override fun partyFromKey(owningKey: CompositeKey): Party? = identityService.partyFromKey(owningKey)
     }
-    class NoPartyObjectMapper: PartyObjectMapper, ObjectMapper() {
+    class NoPartyObjectMapper(factory: JsonFactory): PartyObjectMapper, ObjectMapper(factory) {
         override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
         override fun partyFromKey(owningKey: CompositeKey): Party? = throw UnsupportedOperationException()
     }
@@ -59,7 +57,9 @@ object JacksonSupport {
             addSerializer(BigDecimal::class.java, ToStringSerializer)
             addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
             addSerializer(SecureHash::class.java, SecureHashSerializer)
+            addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
             addDeserializer(SecureHash::class.java, SecureHashDeserializer())
+            addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
             addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
 
             // For ed25519 pubkeys
@@ -86,16 +86,16 @@ object JacksonSupport {
     }
 
     /** Mapper requiring RPC support to deserialise parties from names */
-    @JvmStatic
-    fun createDefaultMapper(rpc: CordaRPCOps): ObjectMapper = configureMapper(RpcObjectMapper(rpc))
+    @JvmStatic @JvmOverloads
+    fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory))
 
     /** For testing or situations where deserialising parties is not required */
-    @JvmStatic
-    fun createNonRpcMapper(): ObjectMapper = configureMapper(NoPartyObjectMapper())
+    @JvmStatic @JvmOverloads
+    fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory))
 
     /** For testing with an in memory identity service */
-    @JvmStatic
-    fun createInMemoryMapper(identityService: IdentityService) = configureMapper(IdentityObjectMapper(identityService))
+    @JvmStatic @JvmOverloads
+    fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory()) = configureMapper(IdentityObjectMapper(identityService, factory))
 
     private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply {
         enable(SerializationFeature.INDENT_OUTPUT)
diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt
new file mode 100644
index 0000000000..15c48a745a
--- /dev/null
+++ b/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt
@@ -0,0 +1,184 @@
+package net.corda.jackson
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall
+import org.slf4j.LoggerFactory
+import java.lang.reflect.Constructor
+import java.lang.reflect.Method
+import java.util.concurrent.Callable
+import javax.annotation.concurrent.ThreadSafe
+import kotlin.reflect.KClass
+import kotlin.reflect.KFunction
+import kotlin.reflect.KotlinReflectionInternalError
+import kotlin.reflect.jvm.kotlinFunction
+
+/**
+ * This class parses strings in a format designed for human usability into [ParsedMethodCall] objects representing a
+ * ready-to-invoke call on the given target object. The strings accepted by this class are a minor variant of
+ * [Yaml](http://www.yaml.org/spec/1.2/spec.html) and can be easily typed at a command line. Intended use cases include
+ * things like the Corda shell, text-based RPC dispatch, simple scripting and so on.
+ *
+ * # Syntax
+ *
+ * The format of the string is as follows. The first word is the name of the method and must always be present. The rest,
+ * which is optional, is wrapped in curly braces and parsed as if it were a Yaml object. The keys of this object are then
+ * mapped to the parameters of the method via the usual Jackson mechanisms. The standard [java.lang.Object] methods are
+ * excluded.
+ *
+ * One convenient feature of Yaml is that barewords collapse into strings, thus you can write a call like the following:
+ *
+ *     fun someCall(note: String, option: Boolean)
+ *
+ *     someCall note: This is a really helpful feature, option: true
+ *
+ * ... and it will be parsed in the intuitive way. Quotes are only needed if you want to put a comma into the string.
+ *
+ * There is an [online Yaml parser](http://yaml-online-parser.appspot.com/) which can be used to explore
+ * the allowed syntax.
+ *
+ * # Usage
+ *
+ * This class is thread safe. Multiple strings may be parsed in parallel, and the resulting [ParsedMethodCall]
+ * objects may be reused multiple times and also invoked in parallel, as long as the underling target object is
+ * thread safe itself.
+ *
+ * You may pass in an alternative [ObjectMapper] to control what types can be parsed, but it must be configured
+ * with the [YAMLFactory] for the class to work.
+ *
+ * # Limitations
+ *
+ * - The target class must be either a Kotlin class, or a Java class compiled with the -parameters command line
+ *   switch, as the class relies on knowing the names of parameters which isn't data provided by default by the
+ *   Java compiler.
+ * - Vararg methods are not supported, as the type information that'd be required is missing.
+ * - Method overloads that have identical parameter names but different types can't be handled, because often
+ *   a string could map to multiple types, so which one to use is ambiguous. If you want your interface to be
+ *   usable with this utility make sure the parameter and method names don't rely on type overloading.
+ *
+ * # Examples
+ *
+ *     fun simple() = ...
+ *     "simple"   -> runs the no-args function 'simple'
+ *
+ *     fun attachmentExists(id: SecureHash): Boolean
+ *     "attachmentExists id: b6d7e826e87"  -> parses the given ID as a SecureHash
+ *
+ *     fun addNote(id: SecureHash, note: String)
+ *     "addNote id: b6d7e826e8739ab2eb6e077fc4fba9b04fb880bb4cbd09bc618d30234a8827a4, note: Some note"
+ */
+@ThreadSafe
+open class StringToMethodCallParser<in T : Any>(targetType: Class<out T>,
+                                                private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory())) {
+    /** Same as the regular constructor but takes a Kotlin reflection [KClass] instead of a Java [Class]. */
+    constructor(targetType: KClass<out T>) : this(targetType.java)
+
+    companion object {
+        @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+        private val ignoredNames = Object::class.java.methods.map { it.name }
+        private fun methodsFromType(clazz: Class<*>) = clazz.methods.map { it.name to it }.toMap().filterKeys { it !in ignoredNames }
+        private val log = LoggerFactory.getLogger(StringToMethodCallParser::class.java)!!
+    }
+
+    /** The methods that can be invoked via this parser. */
+    protected val methodMap = methodsFromType(targetType)
+    /** A map of method name to parameter names for the target type. */
+    val methodParamNames: Map<String, List<String>> = targetType.declaredMethods.mapNotNull {
+        try {
+            it.name to paramNamesFromMethod(it)
+        } catch(e: KotlinReflectionInternalError) {
+            // Kotlin reflection doesn't support every method that can exist on an object (in particular, reified
+            // inline methods) so we just ignore those here.
+            null
+        }
+    }.toMap()
+
+    inner class ParsedMethodCall(private val target: T?, val methodName: String, val args: Array<Any?>) : Callable<Any?> {
+        operator fun invoke(): Any? = call()
+        override fun call(): Any? {
+            if (target == null)
+                throw IllegalStateException("No target object was specified")
+            if (log.isDebugEnabled)
+                log.debug("Invoking call $methodName($args)")
+            return methodMap[methodName]!!.invoke(target, *args)
+        }
+    }
+
+    /**
+     * Uses either Kotlin or Java 8 reflection to learn the names of the parameters to a method.
+     */
+    open fun paramNamesFromMethod(method: Method): List<String> {
+        val kf: KFunction<*>? = method.kotlinFunction
+        return method.parameters.mapIndexed { index, param ->
+            when {
+                param.isNamePresent -> param.name
+                // index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
+                kf != null -> kf.parameters[index + 1].name ?: throw UnparseableCallException.ReflectionDataMissing(method.name, index)
+                else -> throw UnparseableCallException.ReflectionDataMissing(method.name, index)
+            }
+        }
+    }
+
+    /**
+     * Uses either Kotlin or Java 8 reflection to learn the names of the parameters to a constructor.
+     */
+    open fun paramNamesFromConstructor(ctor: Constructor<*>): List<String> {
+        val kf: KFunction<*>? = ctor.kotlinFunction
+        return ctor.parameters.mapIndexed { index, param ->
+            when {
+                param.isNamePresent -> param.name
+                kf != null -> kf.parameters[index].name ?: throw UnparseableCallException.ReflectionDataMissing("<init>", index)
+                else -> throw UnparseableCallException.ReflectionDataMissing("<init>", index)
+            }
+        }
+    }
+
+    open class UnparseableCallException(command: String) : Exception("Could not parse as a command: $command") {
+        class UnknownMethod(val methodName: String) : UnparseableCallException("Unknown command name: $methodName")
+        class MissingParameter(methodName: String, val paramName: String, command: String) : UnparseableCallException("Parameter $paramName missing from attempt to invoke $methodName in command: $command")
+        class TooManyParameters(methodName: String, command: String) : UnparseableCallException("Too many parameters provided for $methodName: $command")
+        class ReflectionDataMissing(methodName: String, argIndex: Int) : UnparseableCallException("Method $methodName missing parameter name at index $argIndex")
+    }
+
+    /**
+     * Parses the given command as a call on the target type. The target should be specified, if it's null then
+     * the resulting [ParsedMethodCall] can't be invoked, just inspected.
+     */
+    @Throws(UnparseableCallException::class)
+    fun parse(target: T?, command: String): ParsedMethodCall {
+        log.debug("Parsing call command from string: {}", command)
+        val spaceIndex = command.indexOf(' ')
+        val name = if (spaceIndex != -1) command.substring(0, spaceIndex) else command
+        val argStr = if (spaceIndex != -1) command.substring(spaceIndex) else ""
+        val method = methodMap[name] ?: throw UnparseableCallException.UnknownMethod(name)
+        log.debug("Parsing call for method {}", name)
+        val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr)
+        return ParsedMethodCall(target, name, args)
+    }
+
+    /**
+     * Parses only the arguments string given the info about parameter names and types.
+     *
+     * @param methodNameHint A name that will be used in exceptions if thrown; not used for any other purpose.
+     */
+    @Throws(UnparseableCallException::class)
+    fun parseArguments(methodNameHint: String, parameters: List<Pair<String, Class<*>>>, args: String): Array<Any?> {
+        // If we have parameters, wrap them in {} to allow the Yaml parser to eat them on a single line.
+        val parameterString = "{ $args }"
+        val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
+        if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
+        val inOrderParams: List<Any?> = parameters.mapIndexed { index, param ->
+            val (argName, argType) = param
+            val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
+            om.readValue(entry.traverse(om), argType)
+        }
+        if (log.isDebugEnabled) {
+            inOrderParams.forEachIndexed { i, param ->
+                val tmp = if (param != null) "${param.javaClass.name} -> $param" else "(null)"
+                log.debug("Parameter $i. $tmp")
+            }
+        }
+        return inOrderParams.toTypedArray()
+    }
+}
\ No newline at end of file
diff --git a/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt b/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt
new file mode 100644
index 0000000000..d0e0c7cbb8
--- /dev/null
+++ b/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt
@@ -0,0 +1,34 @@
+package net.corda.jackson
+
+import net.corda.core.crypto.SecureHash
+import org.junit.Test
+import kotlin.test.assertEquals
+
+class StringToMethodCallParserTest {
+    @Suppress("UNUSED")
+    class Target {
+        fun simple() = "simple"
+        fun string(note: String) = note
+        fun twoStrings(a: String, b: String) = a + b
+        fun simpleObject(hash: SecureHash.SHA256) = hash.toString()!!
+        fun complexObject(pair: Pair<Int, String>) = pair
+    }
+
+    val randomHash = "361170110f61086f77ff2c5b7ab36513705da1a3ebabf14dbe5cc9c982c45401"
+    val tests = mapOf(
+            "simple" to "simple",
+            "string note: A test of barewords" to "A test of barewords",
+            "twoStrings a: Some words, b: ' and some words, like, Kirk, would, speak'" to "Some words and some words, like, Kirk, would, speak",
+            "simpleObject hash: $randomHash" to randomHash.toUpperCase(),
+            "complexObject pair: { first: 12, second: Word up brother }" to Pair(12, "Word up brother")
+    )
+
+    @Test
+    fun calls() {
+        val parser = StringToMethodCallParser(Target::class)
+        val target = Target()
+        for ((input, output) in tests) {
+            assertEquals(output, parser.parse(target, input).invoke())
+        }
+    }
+}
\ No newline at end of file
diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt
index 53f640029e..b5702c7b24 100644
--- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt
+++ b/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt
@@ -1,9 +1,9 @@
 package net.corda.testing.http
 
 import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
 import com.fasterxml.jackson.module.kotlin.KotlinModule
 import net.corda.core.utilities.loggerFor
-import net.corda.jackson.JacksonSupport
 import okhttp3.MediaType
 import okhttp3.OkHttpClient
 import okhttp3.Request
@@ -22,7 +22,7 @@ object HttpUtils {
                 .readTimeout(60, TimeUnit.SECONDS).build()
     }
     val defaultMapper: ObjectMapper by lazy {
-        ObjectMapper().registerModule(JacksonSupport.javaTimeModule).registerModule(KotlinModule())
+        ObjectMapper().registerModule(JavaTimeModule()).registerModule(KotlinModule())
     }
 
     fun putJson(url: URL, data: String) : Boolean {