You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2018/07/16 13:21:18 UTC

[juneau] branch master updated: Add OpenAPI support to remoteable interfaces.

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new a2d9508  Add OpenAPI support to remoteable interfaces.
a2d9508 is described below

commit a2d95084b7eb836a49b97c4f3432cfc659b458d9
Author: JamesBognar <ja...@apache.org>
AuthorDate: Mon Jul 16 09:21:05 2018 -0400

    Add OpenAPI support to remoteable interfaces.
---
 .../juneau/httppart/HttpPartSchemaTest_Body.java   |   5 +-
 .../java/org/apache/juneau/BeanPropertyMeta.java   |   2 +
 .../org/apache/juneau/http/annotation/Body.java    | 101 ++-----
 .../org/apache/juneau/http/annotation/Path.java    |  11 +
 .../org/apache/juneau/httppart/HttpPartSchema.java |  34 +++
 .../juneau/httppart/HttpPartSchemaBuilder.java     |  26 +-
 .../apache/juneau/internal/ReflectionUtils.java    |  29 +++
 .../jsonschema/JsonSchemaSerializerSession.java    |   8 +-
 .../apache/juneau/remoteable/RemoteMethodArg.java  | 115 ++++++--
 .../juneau/remoteable/RemoteMethodBeanArg.java     | 112 ++++++++
 ...emoteMethodArg.java => RemoteMethodReturn.java} |  81 ++++--
 .../juneau/remoteable/RemoteableMethodMeta.java    |  98 ++++---
 .../org/apache/juneau/remoteable/ReturnValue.java  |   5 +-
 .../org/apache/juneau/svl/VarResolverSession.java  |   4 +-
 juneau-doc/src/main/javadoc/overview.html          | 289 +++++++++++++++++++--
 .../rest/test/client/RequestBeanProxyTest.java     |  72 ++---
 .../rest/test/client/ThirdPartyProxyTest.java      |  16 +-
 .../apache/juneau/rest/client/NameValuePairs.java  |   2 +-
 .../org/apache/juneau/rest/client/RestCall.java    | 137 +++++++---
 .../juneau/rest/client/RestCallException.java      |   4 +-
 .../org/apache/juneau/rest/client/RestClient.java  | 122 +++++----
 .../juneau/rest/client/RestClientBuilder.java      |  44 +++-
 .../rest/client/SerializedNameValuePair.java       |   7 +-
 .../org/apache/juneau/rest/BasicRestLogger.java    |   5 +
 .../java/org/apache/juneau/rest/RestContext.java   |   3 +
 .../java/org/apache/juneau/rest/RestLogger.java    |   7 +
 26 files changed, 985 insertions(+), 354 deletions(-)

diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Body.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Body.java
index 4b57053..088424f 100644
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Body.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Body.java
@@ -78,7 +78,6 @@ public class HttpPartSchemaTest_Body {
 	public static class A04 {
 		public void a(
 				@Body(
-					required=false,
 					description={"b3","b3"},
 					schema=@Schema($ref="c3"),
 					example="f2",
@@ -92,8 +91,8 @@ public class HttpPartSchemaTest_Body {
 	@Test
 	public void a04_basic_onParameterAndClass() throws Exception {
 		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, A04.class.getMethod("a", A02.class), 0).noValidate().build();
-		assertNull(s.getRequired());
-		assertObjectEquals("{description:'b3\\nb3',example:'f2',schema:{'$ref':'c3'},_value:'{g2:true}'}", s.getApi());
+		assertTrue(s.getRequired());
+		assertObjectEquals("{description:'b3\\nb3',example:'f2',required:true,schema:{'$ref':'c3'},_value:'{g2:true}'}", s.getApi());
 	}
 
 	@Body(
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
index b6c7770..12ad597 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
@@ -1079,6 +1079,8 @@ public final class BeanPropertyMeta {
 			t = getMethodAnnotation(a, setter);
 		if (t == null && extraKeys != null)
 			t = getMethodAnnotation(a, extraKeys);
+		if (t == null)
+			t = ReflectionUtils.getAnnotation(a, typeMeta.getInnerClass());
 		return t;
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Body.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Body.java
index d281c8c..fa2b005 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Body.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Body.java
@@ -17,8 +17,6 @@ import static java.lang.annotation.RetentionPolicy.*;
 
 import java.io.*;
 import java.lang.annotation.*;
-import java.nio.charset.*;
-import java.util.logging.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.httppart.*;
@@ -45,6 +43,15 @@ import org.apache.juneau.serializer.*;
  * <p>
  * On server-side REST, this annotation can be applied to method parameters or parameter classes to identify them as the body of an HTTP request.
  *
+ * <p>
+ * This annotation can be applied to the following:
+ * <ul class='spaced-list'>
+ * 	<li>
+ * 		Parameters on a <ja>@RestMethod</ja>-annotated method.
+ * 	<li>
+ * 		POJO classes used as parameters on a <ja>@RestMethod</ja>-annotated method.
+ * </ul>
+ *
  * <h5 class='section'>Examples:</h5>
  * <p class='bcode w800'>
  * 	<jc>// Used on parameter</jc>
@@ -70,90 +77,10 @@ import org.apache.juneau.serializer.*;
  * 	}
  * </p>
  *
- * <p>
- * This annotation is also used for supplying swagger information about the body of the request.
- *
- * <h5 class='section'>Examples:</h5>
- * <p class='bcode w800'>
- * 	<jc>// Normal</jc>
- * 	<ja>@Body</ja>(
- * 		description=<js>"Pet object to add to the store"</js>,
- * 		required=<js>"true"</js>,
- * 		example=<js>"{name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']}"</js>
- * 	)
- * </p>
- * <p class='bcode w800'>
- * 	<jc>// Free-form</jc>
- * 	<ja>@Body</ja>({
- * 		<js>"description: 'Pet object to add to the store',"</js>,
- * 		<js>"required: true,"</js>,
- * 		<js>"example: {name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']},"</js>
- * 		<js>"x-extra: 'extra field'"</js>
- * 	})
- * </p>
- *
- * <p>
- * This is used to populate the auto-generated Swagger documentation and UI.
- *
- * <p>
- * This annotation can be applied to the following:
- * <ul class='spaced-list'>
- * 	<li>
- * 		Parameters on a <ja>@RestMethod</ja>-annotated method.
- * 	<li>
- * 		POJO classes used as parameters on a <ja>@RestMethod</ja>-annotated method.
- * </ul>
- *
- * <p>
- * Any of the following types can be used (matched in the specified order):
- * <ol class='spaced-list'>
- * 	<li>
- * 		{@link Reader}
- * 		<br><ja>@Body</ja> annotation is optional (it's inferred from the class type).
- * 		<br><code>Content-Type</code> is always ignored.
- * 	<li>
- * 		{@link InputStream}
- * 		<br><ja>@Body</ja> annotation is optional (it's inferred from the class type).
- * 		<br><code>Content-Type</code> is always ignored.
- * 	<li>
- * 		Any <a class='doclink' href='../../../../../overview-summary.html#juneau-marshall.PojoCategories'>parsable</a> POJO type.
- * 		<br><code>Content-Type</code> is required to identify correct parser.
- * 	<li>
- * 		Objects convertible from {@link Reader} by having one of the following non-deprecated methods:
- * 		<ul>
- * 			<li><code><jk>public</jk> T(Reader in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>create</jsm>(Reader in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>fromReader</jsm>(Reader in) {...}</code>
- * 		</ul>
- * 		<br><code>Content-Type</code> must not be present or match an existing parser so that it's not parsed as a POJO.
- * 	<li>
- * 		Objects convertible from {@link InputStream} by having one of the following non-deprecated methods:
- * 		<ul>
- * 			<li><code><jk>public</jk> T(InputStream in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>create</jsm>(InputStream in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>fromInputStream</jsm>(InputStream in) {...}</code>
- * 		</ul>
- * 		<br><code>Content-Type</code> must not be present or match an existing parser so that it's not parsed as a POJO.
- * 	<li>
- * 		Objects convertible from {@link String} (including <code>String</code> itself) by having one of the following non-deprecated methods:
- * 		<ul>
- * 			<li><code><jk>public</jk> T(String in) {...}</code> (e.g. {@link Integer}, {@link Boolean})
- * 			<li><code><jk>public static</jk> T <jsm>create</jsm>(String in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>fromString</jsm>(String in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>fromValue</jsm>(String in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>valueOf</jsm>(String in) {...}</code> (e.g. enums)
- * 			<li><code><jk>public static</jk> T <jsm>parse</jsm>(String in) {...}</code> (e.g. {@link Level})
- * 			<li><code><jk>public static</jk> T <jsm>parseString</jsm>(String in) {...}</code>
- * 			<li><code><jk>public static</jk> T <jsm>forName</jsm>(String in) {...}</code> (e.g. {@link Class}, {@link Charset})
- * 			<li><code><jk>public static</jk> T <jsm>forString</jsm>(String in) {...}</code>
- * 		</ul>
- * 		<br><code>Content-Type</code> must not be present or match an existing parser so that it's not parsed as a POJO.
- * </ol>
- *
  * <h5 class='section'>Notes:</h5>
  * <ul class='spaced-list'>
  * 	<li>
- * 		Annotation values are coalesced from multiple sources in the following order of precedence:
+ * 		Swagger values are coalesced from multiple sources in the following order of precedence:
  * 		<ol>
  * 			<li><ja>@Body</ja> annotation on parameter.
  * 			<li><ja>@Body</ja> annotation on parameter class.
@@ -225,7 +152,7 @@ import org.apache.juneau.serializer.*;
  *
  * <h5 class='section'>See Also:</h5>
  * <ul class='doctree'>
- * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-rest-client.3rdPartyProxies'>Overview &gt; juneau-rest-client &gt; Interface Proxies Against 3rd-party REST Interfaces</a>
+ * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-rest-client.3rdPartyProxies.Body'>Overview &gt; juneau-rest-client &gt; Interface Proxies Against 3rd-party REST Interfaces &gt; Body</a>
  * </ul>
  */
 @Documented
@@ -233,6 +160,7 @@ import org.apache.juneau.serializer.*;
 @Retention(RUNTIME)
 @Inherited
 public @interface Body {
+
 	//=================================================================================================================
 	// Attributes common to all Swagger Parameter objects
 	//=================================================================================================================
@@ -562,6 +490,11 @@ public @interface Body {
 	String[] api() default {};
 
 	/**
+	 * TODO
+	 */
+	Class<? extends HttpPartSerializer> serializer() default HttpPartSerializer.Null.class;
+
+	/**
 	 * Specifies the {@link HttpPartParser} class used for parsing values from strings.
 	 *
 	 * <p>
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
index 7a17097..348e3ad 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
@@ -399,6 +399,17 @@ public @interface Path {
 	String format() default "";
 
 	/**
+	 * <mk>allowEmptyValue</mk> field of the Swagger <a class="doclink" href="https://swagger.io/specification/v2/#parameterObject">Parameter</a> object.
+	 *
+	 * <p>
+	 * Sets the ability to pass empty-valued heaver values.
+	 *
+	 * <p>
+	 * <b>Note:</b>  This is technically only valid for either query or formData parameters, but support is provided anyway for backwards compatability.
+	 */
+	boolean allowEmptyValue() default false;
+
+	/**
 	 * <mk>items</mk> field of the Swagger <a class="doclink" href="https://swagger.io/specification/v2/#parameterObject">Parameter</a> object.
 	 *
 	 * <p>
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
index c4dc13a..5dd9ff8 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchema.java
@@ -115,6 +115,40 @@ public class HttpPartSchema {
 	}
 
 	/**
+	 * Finds the schema information for the specified method return.
+	 *
+	 * <p>
+	 * This method will gather all the schema information from the annotations at the following locations:
+	 * <ul>
+	 * 	<li>The method.
+	 * 	<li>The method return class.
+	 * 	<li>The method return parent classes and interfaces.
+	 * </ul>
+	 *
+	 * @param c
+	 * 	The annotation to look for.
+	 * 	<br>Valid values:
+	 * 	<ul>
+	 * 		<li>{@link Body}
+	 * 		<li>{@link Header}
+	 * 		<li>{@link Query}
+	 * 		<li>{@link FormData}
+	 * 		<li>{@link Path}
+	 * 		<li>{@link Response}
+	 * 		<li>{@link ResponseHeader}
+	 * 		<li>{@link ResponseStatus}
+	 * 		<li>{@link HasQuery}
+	 * 		<li>{@link HasFormData}
+	 * 	</ul>
+	 * @param m
+	 * 	The Java method with the return type being checked.
+	 * @return The schema information about the parameter.
+	 */
+	public static HttpPartSchema create(Class<? extends Annotation> c, Method m) {
+		return create().apply(c, m).build();
+	}
+
+	/**
 	 * Finds the schema information for the specified class.
 	 *
 	 * <p>
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java
index d3479cc..16c0dcf 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java
@@ -63,10 +63,18 @@ public class HttpPartSchemaBuilder {
 	}
 
 	HttpPartSchemaBuilder apply(Class<? extends Annotation> c, Method m, int index) {
+		apply(c, m.getGenericParameterTypes()[index]);
 		for (Annotation a :  m.getParameterAnnotations()[index])
 			if (c.isInstance(a))
-				return apply(a);
-		apply(c, m.getGenericParameterTypes()[index]);
+				apply(a);
+		return this;
+	}
+
+	HttpPartSchemaBuilder apply(Class<? extends Annotation> c, Method m) {
+		apply(c, m.getGenericReturnType());
+		Annotation a = m.getAnnotation(c);
+		if (a != null)
+			return apply(a);
 		return this;
 	}
 
@@ -77,7 +85,13 @@ public class HttpPartSchemaBuilder {
 		return this;
 	}
 
-	HttpPartSchemaBuilder apply(Annotation a) {
+	/**
+	 * Apply the specified annotation to this schema.
+	 *
+	 * @param a The annotation to apply.
+	 * @return This object (for method chaining).
+	 */
+	public HttpPartSchemaBuilder apply(Annotation a) {
 		if (a instanceof Body)
 			apply((Body)a);
 		else if (a instanceof Header)
@@ -105,6 +119,7 @@ public class HttpPartSchemaBuilder {
 		api = AnnotationUtils.merge(api, a);
 		required(HttpPartSchema.toBoolean(a.required()));
 		allowEmptyValue(HttpPartSchema.toBoolean(! a.required()));
+		serializer(a.serializer());
 		parser(a.parser());
 		apply(a.schema());
 		return this;
@@ -238,6 +253,7 @@ public class HttpPartSchemaBuilder {
 		type(a.type());
 		format(a.format());
 		items(a.items());
+		allowEmptyValue(HttpPartSchema.toBoolean(a.allowEmptyValue()));
 		collectionFormat(a.collectionFormat());
 		maximum(HttpPartSchema.toNumber(a.maximum()));
 		exclusiveMaximum(HttpPartSchema.toBoolean(a.exclusiveMaximum()));
@@ -1332,7 +1348,7 @@ public class HttpPartSchemaBuilder {
 	 * @return This object (for method chaining).
 	 */
 	public HttpPartSchemaBuilder serializer(Class<? extends HttpPartSerializer> value) {
-		if (serializer != null && serializer != HttpPartSerializer.Null.class)
+		if (value != null && value != HttpPartSerializer.Null.class)
 			serializer = value;
 		return this;
 	}
@@ -1346,7 +1362,7 @@ public class HttpPartSchemaBuilder {
 	 * @return This object (for method chaining).
 	 */
 	public HttpPartSchemaBuilder parser(Class<? extends HttpPartParser> value) {
-		if (parser != null && parser != HttpPartParser.Null.class)
+		if (value != null && value != HttpPartParser.Null.class)
 			parser = value;
 		return this;
 	}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ReflectionUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ReflectionUtils.java
index 2cec856..0464c3f 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ReflectionUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ReflectionUtils.java
@@ -37,6 +37,17 @@ public final class ReflectionUtils {
 	}
 
 	/**
+	 * Returns <jk>true</jk> if the {@link #getAnnotation(Class, Method)} returns a value.
+	 *
+	 * @param a The annotation to check for.
+	 * @param m The method to check.
+	 * @return <jk>true</jk> if the {@link #getAnnotation(Class, Method)} returns a value.
+	 */
+	public static boolean hasAnnotation(Class<? extends Annotation> a, Method m) {
+		return getAnnotation(a, m) != null;
+	}
+
+	/**
 	 * Returns the specified annotation if it exists on the specified parameter or parameter type class.
 	 *
 	 * @param a The annotation to check for.
@@ -56,6 +67,24 @@ public final class ReflectionUtils {
 	}
 
 	/**
+	 * Returns the specified annotation if it exists on the specified method or return type class.
+	 *
+	 * @param a The annotation to check for.
+	 * @param m The method to check.
+	 * @return <jk>true</jk> if the {@link #getAnnotation(Class, Method, int)} returns a value.
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T extends Annotation> T getAnnotation(Class<T> a, Method m) {
+		for (Annotation a2 :  m.getAnnotations())
+			if (a.isInstance(a2))
+				return (T)a2;
+		Type t = m.getGenericReturnType();
+		if (t instanceof Class)
+			return getAnnotation(a, (Class<?>)t);
+		return null;
+	}
+
+	/**
 	 * Similar to {@link Class#getAnnotation(Class)} except also searches annotations on interfaces.
 	 *
 	 * @param <T> The annotation class type.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/JsonSchemaSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/JsonSchemaSerializerSession.java
index c6f19db..e4fb8f8 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/JsonSchemaSerializerSession.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/JsonSchemaSerializerSession.java
@@ -109,12 +109,16 @@ public class JsonSchemaSerializerSession extends JsonSerializerSession {
 			descriptionAdded = false;
 		}
 
-		if (useDef && defs.containsKey(getBeanDefId(sType)))
+		if (useDef && defs.containsKey(getBeanDefId(sType))) {
+			pop();
 			return new ObjectMap().append("$ref", getBeanDefUri(sType));
+		}
 
 		ObjectMap ds = getDefaultSchemas().get(sType.getInnerClass().getName());
-		if (ds != null && ds.containsKey("type"))
+		if (ds != null && ds.containsKey("type")) {
+			pop();
 			return out.appendAll(ds);
+		}
 
 		JsonSchemaClassMeta jscm = null;
 		if (pojoSwap != null && pojoSwap.getClass().getAnnotation(JsonSchema.class) != null)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java
index f188334..b715b85 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java
@@ -13,9 +13,14 @@
 package org.apache.juneau.remoteable;
 
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.ReflectionUtils.*;
+import static org.apache.juneau.httppart.HttpPartType.*;
 
+import java.lang.reflect.*;
+
+import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
-import org.apache.juneau.urlencoding.*;
+import org.apache.juneau.internal.*;
 
 /**
  * Represents the metadata about an annotated argument of a method on a remote proxy interface.
@@ -25,35 +30,99 @@ import org.apache.juneau.urlencoding.*;
  * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-rest-client.3rdPartyProxies'>Overview &gt; juneau-rest-client &gt; Interface Proxies Against 3rd-party REST Interfaces</a>
  * </ul>
  */
-public class RemoteMethodArg {
+public final class RemoteMethodArg {
+
+	private final int index;
+	private final HttpPartType partType;
+	private final HttpPartSerializer serializer;
+	private final HttpPartSchema schema;
+	private final String name;
+	private final boolean skipIfEmpty;
 
-	/** The argument name.  Can be blank. */
-	public final String name;
+	RemoteMethodArg(int index, HttpPartType partType, HttpPartSchema schema) {
+		this.index = index;
+		this.partType = partType;
+		this.serializer = newInstance(HttpPartSerializer.class, schema == null ? null : schema.getSerializer());
+		this.schema = schema;
+		this.name = schema == null ? null : schema.getName();
+		this.skipIfEmpty = schema == null || schema.getSkipIfEmpty() == null ? false : schema.getSkipIfEmpty();
+	}
 
-	/** The zero-based index of the argument on the Java method. */
-	public final int index;
+	RemoteMethodArg(HttpPartType partType, HttpPartSchema schema, String defaultName) {
+		this.index = -1;
+		this.partType = partType;
+		this.serializer = newInstance(HttpPartSerializer.class, schema == null ? null : schema.getSerializer());
+		this.schema = schema;
+		this.name = StringUtils.firstNonEmpty(schema == null ? null : schema.getName(), defaultName);
+		this.skipIfEmpty = schema == null || schema.getSkipIfEmpty() == null ? false : schema.getSkipIfEmpty();
+	}
 
-	/** The value is skipped if it's null/empty. */
-	public final boolean skipIfNE;
+	/**
+	 * Returns the name of the HTTP part.
+	 *
+	 * @return The name of the HTTP part.
+	 */
+	public String getName() {
+		return name;
+	}
 
-	/** The serializer used for converting objects to strings. */
-	public final HttpPartSerializer serializer;
+	/**
+	 * Returns whether the <code>skipIfEmpty</code> flag was found in the schema.
+	 *
+	 * @return <jk>true</jk> if the <code>skipIfEmpty</code> flag was found in the schema.
+	 */
+	public boolean isSkipIfEmpty() {
+		return skipIfEmpty;
+	}
 
 	/**
-	 * Constructor.
+	 * Returns the method argument index.
 	 *
-	 * @param name The argument name pulled from name().
-	 * @param name2 The argument name pulled from value().
-	 * @param index The zero-based index of the argument on the Java method.
-	 * @param skipIfNE The value is skipped if it's null/empty.
-	 * @param serializer
-	 * 	The class to use for serializing headers, query parameters, form-data parameters, and path variables.
-	 * 	If {@link UrlEncodingSerializer}, then the url-encoding serializer defined on the client will be used.
+	 * @return The method argument index.
 	 */
-	protected RemoteMethodArg(String name, String name2, int index, boolean skipIfNE, Class<? extends HttpPartSerializer> serializer) {
-		this.name = name.isEmpty() ? name2 : name;
-		this.index = index;
-		this.skipIfNE = skipIfNE;
-		this.serializer = newInstance(HttpPartSerializer.class, serializer);
+	public int getIndex() {
+		return index;
+	}
+
+	/**
+	 * Returns the HTTP part type.
+	 *
+	 * @return The HTTP part type.  Never <jk>null</jk>.
+	 */
+	public HttpPartType getPartType() {
+		return partType;
+	}
+
+	/**
+	 * Returns the HTTP part serializer to use for serializing this part.
+	 *
+	 * @return The HTTP part serializer, or <jk>null</jk> if not specified.
+	 */
+	public HttpPartSerializer getSerializer() {
+		return serializer;
+	}
+
+	/**
+	 * Returns the HTTP part schema information about this part.
+	 *
+	 * @return The HTTP part schema information, or <jk>null</jk> if not found.
+	 */
+	public HttpPartSchema getSchema() {
+		return schema;
+	}
+
+	static RemoteMethodArg create(Method m, int i) {
+		if (hasAnnotation(Header.class, m, i)) {
+			return new RemoteMethodArg(i, HEADER, HttpPartSchema.create(Header.class, m, i));
+		} else if (hasAnnotation(Query.class, m, i)) {
+			return new RemoteMethodArg(i, QUERY, HttpPartSchema.create(Query.class, m, i));
+		} else if (hasAnnotation(FormData.class, m, i)) {
+			return new RemoteMethodArg(i, FORMDATA, HttpPartSchema.create(FormData.class, m, i));
+		} else if (hasAnnotation(Path.class, m, i)) {
+			return new RemoteMethodArg(i, PATH, HttpPartSchema.create(Path.class, m, i));
+		} else if (hasAnnotation(Body.class, m, i)) {
+			return new RemoteMethodArg(i, BODY, HttpPartSchema.create(Body.class, m, i));
+		}
+		return null;
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodBeanArg.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodBeanArg.java
new file mode 100644
index 0000000..c682a29
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodBeanArg.java
@@ -0,0 +1,112 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you 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.apache.juneau.remoteable;
+
+import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.ReflectionUtils.*;
+import static org.apache.juneau.httppart.HttpPartType.*;
+
+import java.lang.annotation.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.httppart.*;
+
+/**
+ * Represents the metadata about an {@link RequestBean}-annotated argument of a method on a remote proxy interface.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul class='doctree'>
+ * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-rest-client.3rdPartyProxies'>Overview &gt; juneau-rest-client &gt; Interface Proxies Against 3rd-party REST Interfaces</a>
+ * </ul>
+ */
+public final class RemoteMethodBeanArg {
+
+	private final int index;
+	private final HttpPartSerializer serializer;
+	private final Map<String,RemoteMethodArg> properties;
+
+	private RemoteMethodBeanArg(int index, Class<? extends HttpPartSerializer> serializer, Map<String,RemoteMethodArg> properties) {
+		this.index = index;
+		this.serializer = newInstance(HttpPartSerializer.class, serializer);
+		this.properties = properties;
+	}
+
+	/**
+	 * Returns the index of the parameter in the method that is a request bean.
+	 *
+	 * @return The index of the parameter in the method that is a request bean.
+	 */
+	public int getIndex() {
+		return index;
+	}
+
+	/**
+	 * Returns the serializer to use for serializing parts on the request bean.
+	 *
+	 * @return The serializer to use for serializing parts on the request bean, or <jk>null</jk> if not defined.
+	 */
+	public HttpPartSerializer getSerializer() {
+		return serializer;
+	}
+
+	/**
+	 * Returns metadata on the specified property of the request bean.
+	 *
+	 * @param name The bean property name.
+	 * @return Metadata about the bean property, or <jk>null</jk> if not found.
+	 */
+	public RemoteMethodArg getProperty(String name) {
+		return properties.get(name);
+	}
+
+	static RemoteMethodBeanArg create(Method m, int i) {
+		Map<String,RemoteMethodArg> map = new LinkedHashMap<>();
+		if (hasAnnotation(RequestBean.class, m, i)) {
+			RequestBean rb = getAnnotation(RequestBean.class, m, i);
+			BeanMeta<?> bm = BeanContext.DEFAULT.getBeanMeta(m.getParameterTypes()[i]);
+			for (BeanPropertyMeta bp : bm.getPropertyMetas()) {
+				String n = bp.getName();
+				Annotation a = bp.getAnnotation(Path.class);
+				if (a != null) {
+					HttpPartSchema s = HttpPartSchema.create().apply(a).build();
+					map.put(n, new RemoteMethodArg(PATH, s, n));
+				}
+				a = bp.getAnnotation(Header.class);
+				if (a != null) {
+					HttpPartSchema s = HttpPartSchema.create().apply(a).build();
+					map.put(n, new RemoteMethodArg(HEADER, s, n));
+				}
+				a = bp.getAnnotation(Query.class);
+				if (a != null) {
+					HttpPartSchema s = HttpPartSchema.create().apply(a).build();
+					map.put(n, new RemoteMethodArg(QUERY, s, n));
+				}
+				a = bp.getAnnotation(FormData.class);
+				if (a != null) {
+					HttpPartSchema s = HttpPartSchema.create().apply(a).build();
+					map.put(n, new RemoteMethodArg(FORMDATA, s, n));
+				}
+				a = bp.getAnnotation(Body.class);
+				if (a != null) {
+					HttpPartSchema s = HttpPartSchema.create().apply(a).build();
+					map.put(n, new RemoteMethodArg(BODY, s, n));
+				}
+			}
+			return new RemoteMethodBeanArg(i, rb.serializer(), map);
+		}
+		return null;
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodReturn.java
similarity index 53%
copy from juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodReturn.java
index f188334..8604190 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodReturn.java
@@ -13,47 +13,80 @@
 package org.apache.juneau.remoteable;
 
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.ReflectionUtils.*;
+import static org.apache.juneau.remoteable.ReturnValue.*;
 
+import java.lang.reflect.*;
+
+import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
-import org.apache.juneau.urlencoding.*;
 
 /**
- * Represents the metadata about an annotated argument of a method on a remote proxy interface.
+ * Represents the metadata about the returned object of a method on a remote proxy interface.
  *
  * <h5 class='section'>See Also:</h5>
  * <ul class='doctree'>
  * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-rest-client.3rdPartyProxies'>Overview &gt; juneau-rest-client &gt; Interface Proxies Against 3rd-party REST Interfaces</a>
  * </ul>
  */
-public class RemoteMethodArg {
+public final class RemoteMethodReturn {
 
-	/** The argument name.  Can be blank. */
-	public final String name;
+	private final HttpPartParser parser;
+	private final HttpPartSchema schema;
+	private final Type returnType;
+	private final ReturnValue returnValue;
 
-	/** The zero-based index of the argument on the Java method. */
-	public final int index;
+	RemoteMethodReturn(Method m, ReturnValue returnValue) {
+		this.returnType = m.getGenericReturnType();
+		if (hasAnnotation(Body.class, m)) {
+			this.schema = HttpPartSchema.create(Body.class, m);
+			this.parser = newInstance(HttpPartParser.class, schema.getParser());
+		} else {
+			this.schema = null;
+			this.parser = null;
+		}
+		if (returnValue == null) {
+			if (m.getReturnType() == void.class)
+				returnValue = NONE;
+			else
+				returnValue = BODY;
+		}
+		this.returnValue = returnValue;
+	}
 
-	/** The value is skipped if it's null/empty. */
-	public final boolean skipIfNE;
+	/**
+	 * Returns the parser to use for parsing this part.
+	 *
+	 * @return The parser to use for parsing this part, or <jk>null</jk> if not specified.
+	 */
+	public HttpPartParser getParser() {
+		return parser;
+	}
 
-	/** The serializer used for converting objects to strings. */
-	public final HttpPartSerializer serializer;
+	/**
+	 * Returns schema information about the HTTP part.
+	 *
+	 * @return Schema information about the HTTP part, or <jk>null</jk> if not found.
+	 */
+	public HttpPartSchema getSchema() {
+		return schema;
+	}
+
+	/**
+	 * Returns the class type of the method return.
+	 *
+	 * @return The class type of the method return.
+	 */
+	public Type getReturnType() {
+		return returnType;
+	}
 
 	/**
-	 * Constructor.
+	 * Specifies whether the return value is the body of the request or the HTTP status.
 	 *
-	 * @param name The argument name pulled from name().
-	 * @param name2 The argument name pulled from value().
-	 * @param index The zero-based index of the argument on the Java method.
-	 * @param skipIfNE The value is skipped if it's null/empty.
-	 * @param serializer
-	 * 	The class to use for serializing headers, query parameters, form-data parameters, and path variables.
-	 * 	If {@link UrlEncodingSerializer}, then the url-encoding serializer defined on the client will be used.
+	 * @return The type of value returned.
 	 */
-	protected RemoteMethodArg(String name, String name2, int index, boolean skipIfNE, Class<? extends HttpPartSerializer> serializer) {
-		this.name = name.isEmpty() ? name2 : name;
-		this.index = index;
-		this.skipIfNE = skipIfNE;
-		this.serializer = newInstance(HttpPartSerializer.class, serializer);
+	public ReturnValue getReturnValue() {
+		return returnValue;
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMethodMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMethodMeta.java
index 28516f9..ca3a71a 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMethodMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMethodMeta.java
@@ -14,12 +14,13 @@ package org.apache.juneau.remoteable;
 
 import static org.apache.juneau.internal.ClassUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.httppart.HttpPartType.*;
 
-import java.lang.annotation.*;
 import java.lang.reflect.*;
 import java.util.*;
 
 import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.httppart.*;
 
 /**
  * Contains the meta-data about a Java method on a remoteable interface.
@@ -36,10 +37,10 @@ public class RemoteableMethodMeta {
 
 	private final String httpMethod;
 	private final String url;
-	private final RemoteMethodArg[] pathArgs, queryArgs, headerArgs, formDataArgs, requestBeanArgs;
-	private final Integer[] otherArgs;
-	private final Integer bodyArg;
-	private final ReturnValue returnValue;
+	private final RemoteMethodArg[] pathArgs, queryArgs, headerArgs, formDataArgs, otherArgs;
+	private final RemoteMethodBeanArg[] requestBeanArgs;
+	private final RemoteMethodArg bodyArg;
+	private final RemoteMethodReturn methodReturn;
 
 	/**
 	 * Constructor.
@@ -55,10 +56,10 @@ public class RemoteableMethodMeta {
 		this.queryArgs = b.queryArgs.toArray(new RemoteMethodArg[b.queryArgs.size()]);
 		this.formDataArgs = b.formDataArgs.toArray(new RemoteMethodArg[b.formDataArgs.size()]);
 		this.headerArgs = b.headerArgs.toArray(new RemoteMethodArg[b.headerArgs.size()]);
-		this.requestBeanArgs = b.requestBeanArgs.toArray(new RemoteMethodArg[b.requestBeanArgs.size()]);
-		this.otherArgs = b.otherArgs.toArray(new Integer[b.otherArgs.size()]);
+		this.requestBeanArgs = b.requestBeanArgs.toArray(new RemoteMethodBeanArg[b.requestBeanArgs.size()]);
+		this.otherArgs = b.otherArgs.toArray(new RemoteMethodArg[b.otherArgs.size()]);
 		this.bodyArg = b.bodyArg;
-		this.returnValue = b.returnValue;
+		this.methodReturn = b.methodReturn;
 	}
 
 	private static final class Builder {
@@ -68,11 +69,11 @@ public class RemoteableMethodMeta {
 			queryArgs = new LinkedList<>(),
 			headerArgs = new LinkedList<>(),
 			formDataArgs = new LinkedList<>(),
-			requestBeanArgs = new LinkedList<>();
-		List<Integer>
 			otherArgs = new LinkedList<>();
-		Integer bodyArg;
-		ReturnValue returnValue;
+		List<RemoteMethodBeanArg>
+			requestBeanArgs = new LinkedList<>();
+		RemoteMethodArg bodyArg;
+		RemoteMethodReturn methodReturn;
 
 		Builder(String restUrl, Method m) {
 			Remoteable r = m.getDeclaringClass().getAnnotation(Remoteable.class);
@@ -90,50 +91,43 @@ public class RemoteableMethodMeta {
 				throw new RemoteableMetadataException(m,
 					"Invalid value specified for @Remoteable.methodPaths() annotation.  Valid values are [NAME,SIGNATURE].");
 
-			returnValue = rm == null ? ReturnValue.BODY : rm.returns();
+			ReturnValue rv = m.getReturnType() == void.class ? ReturnValue.NONE : rm == null ? ReturnValue.BODY : rm.returns();
+
+			methodReturn = new RemoteMethodReturn(m, rv);
 
 			url =
 				trimSlashes(restUrl)
 				+ '/'
 				+ (path != null ? trimSlashes(path) : urlEncode("NAME".equals(methodPaths) ? m.getName() : getMethodSignature(m)));
 
-			int index = 0;
-			for (Annotation[] aa : m.getParameterAnnotations()) {
+			for (int i = 0; i < m.getParameterTypes().length; i++) {
+				RemoteMethodArg rma = RemoteMethodArg.create(m, i);
 				boolean annotated = false;
-				for (Annotation a : aa) {
-					Class<?> ca = a.annotationType();
-					if (ca == Path.class) {
-						Path p = (Path)a;
-						annotated = pathArgs.add(new RemoteMethodArg(p.name(), p.value(), index, false, p.serializer()));
-					} else if (ca == Query.class) {
-						Query q = (Query)a;
-						annotated = queryArgs.add(new RemoteMethodArg(q.name(), q.value(), index, q.skipIfEmpty(), q.serializer()));
-					} else if (ca == FormData.class) {
-						FormData f = (FormData)a;
-						annotated = formDataArgs.add(new RemoteMethodArg(f.name(), f.value(), index, f.skipIfEmpty(), f.serializer()));
-					} else if (ca == Header.class) {
-						Header h = (Header)a;
-						annotated = headerArgs.add(new RemoteMethodArg(h.name(), h.value(), index, h.skipIfEmpty(), h.serializer()));
-					} else if (ca == RequestBean.class) {
-						RequestBean rb = (RequestBean)a;
-						annotated = requestBeanArgs.add(new RemoteMethodArg("", "", index, false, rb.serializer()));
-					} else if (ca == Body.class) {
-						annotated = true;
-						if (bodyArg == null)
-							bodyArg = index;
-						else
-							throw new RemoteableMetadataException(m,
-								"Multiple @Body parameters found.  Only one can be specified per Java method.");
-					}
+				if (rma != null) {
+					annotated = true;
+					HttpPartType pt = rma.getPartType();
+					if (pt == HEADER)
+						headerArgs.add(rma);
+					else if (pt == QUERY)
+						queryArgs.add(rma);
+					else if (pt == FORMDATA)
+						formDataArgs.add(rma);
+					else if (pt == PATH)
+						pathArgs.add(rma);
+					else if (pt == BODY)
+						bodyArg = rma;
+					else
+						annotated = false;
+				}
+				RemoteMethodBeanArg rmba = RemoteMethodBeanArg.create(m, i);
+				if (rmba != null) {
+					annotated = true;
+					requestBeanArgs.add(rmba);
+				}
+				if (! annotated) {
+					otherArgs.add(new RemoteMethodArg(i, BODY, null));
 				}
-				if (! annotated)
-					otherArgs.add(index);
-				index++;
 			}
-
-			if (bodyArg != null && otherArgs.size() > 0)
-				throw new RemoteableMetadataException(m,
-					"@Body and non-annotated parameters found together.  Non-annotated parameters cannot be used when @Body is used.");
 		}
 	}
 
@@ -196,7 +190,7 @@ public class RemoteableMethodMeta {
 	 *
 	 * @return A list of zero-indexed argument indices.
 	 */
-	public RemoteMethodArg[] getRequestBeanArgs() {
+	public RemoteMethodBeanArg[] getRequestBeanArgs() {
 		return requestBeanArgs;
 	}
 
@@ -205,7 +199,7 @@ public class RemoteableMethodMeta {
 	 *
 	 * @return A list of zero-indexed argument indices.
 	 */
-	public Integer[] getOtherArgs() {
+	public RemoteMethodArg[] getOtherArgs() {
 		return otherArgs;
 	}
 
@@ -214,7 +208,7 @@ public class RemoteableMethodMeta {
 	 *
 	 * @return A index of the argument with the {@link Body @Body} annotation, or <jk>null</jk> if no argument exists.
 	 */
-	public Integer getBodyArg() {
+	public RemoteMethodArg getBodyArg() {
 		return bodyArg;
 	}
 
@@ -223,7 +217,7 @@ public class RemoteableMethodMeta {
 	 *
 	 * @return Whether the method returns the HTTP response body or status code.
 	 */
-	public ReturnValue getReturns() {
-		return returnValue;
+	public RemoteMethodReturn getReturns() {
+		return methodReturn;
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/ReturnValue.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/ReturnValue.java
index c438fb0..1c92b6b 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/ReturnValue.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/ReturnValue.java
@@ -21,5 +21,8 @@ public enum ReturnValue {
 	BODY,
 
 	/** HTTP status code */
-	HTTP_STATUS;
+	HTTP_STATUS,
+
+	/** Ignore (used for void methods) */
+	NONE;
 }
diff --git a/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverSession.java b/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverSession.java
index fb8c6f0..ee13c93 100644
--- a/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverSession.java
+++ b/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverSession.java
@@ -109,7 +109,7 @@ public class VarResolverSession {
 				} catch (VarResolverException e) {
 					throw e;
 				} catch (Exception e) {
-					throw new VarResolverException(e, "Problem occurred resolving variable ''{0}''", var);
+					throw new VarResolverException(e, "Problem occurred resolving variable ''{0}'' in string ''{1}''", var, s);
 				}
 			}
 			return s;
@@ -353,7 +353,7 @@ public class VarResolverSession {
 							} catch (VarResolverException e) {
 								throw e;
 							} catch (Exception e) {
-								throw new VarResolverException(e, "Problem occurred resolving variable ''{0}''", varType);
+								throw new VarResolverException(e, "Problem occurred resolving variable ''{0}'' in string ''{1}''", varType, s);
 							}
 							x = i+1;
 						}
diff --git a/juneau-doc/src/main/javadoc/overview.html b/juneau-doc/src/main/javadoc/overview.html
index b89af1d..ee13f83 100644
--- a/juneau-doc/src/main/javadoc/overview.html
+++ b/juneau-doc/src/main/javadoc/overview.html
@@ -369,6 +369,12 @@
 	<li><p class='toc2'><a class='doclink' href='#juneau-rest-client'><i>juneau-rest-client</i></a></p>
 	<ol>
 		<li><p><a class='doclink' href='#juneau-rest-client.3rdPartyProxies'>Interface Proxies Against 3rd-party REST Interfaces</a></p>
+		<ul>
+			<li><p><a class='doclink' href='#juneau-rest-client.3rdPartyProxies.Body'>@Body</a></p>
+			<li><p><a class='doclink' href='#juneau-rest-client.3rdPartyProxies.FormData'>@FormData</a></p>
+			<li><p><a class='doclink' href='#juneau-rest-client.3rdPartyProxies.Query'>@Query</a></p>
+			<li><p><a class='doclink' href='#juneau-rest-client.3rdPartyProxies.Header'>@Header</a></p>
+		</ul>
 		<li><p><a class='doclink' href='#juneau-rest-client.SSL'>SSL Support</a></p>
 		<li><p><a class='doclink' href='#juneau-rest-client.Authentication'>Authentication</a></p>
 		<ol>
@@ -12791,9 +12797,9 @@
 		</p>
 		<ul class='spaced-list'>
 			<li>
-				Parameters on a  {@link org.apache.juneau.rest.annotation.RestMethod @RestMethod}.
+				Parameters on a <ja>@RestMethod</ja>-annotated method.
 			<li>
-				POJO classes.
+				POJO classes used as parameters on a <ja>@RestMethod</ja>-annotated method.
 		</ul>
 		<h5 class='figure'>Examples:</h5>
 		<p class='bcode w800'>
@@ -12927,6 +12933,36 @@
 		}
 	}
 		</p>
+		<p>
+			This annotation is also used for supplying swagger information about the body of the request.
+			<br>This information is used to populate the auto-generated Swagger documentation and UI.
+		</p>
+		<h5 class='section'>Examples:</h5>
+		<p class='bcode w800'>
+	<jc>// Normal</jc>
+	<ja>@Body</ja>(
+		description=<js>"Pet object to add to the store"</js>,
+		required=<js>"true"</js>,
+		example=<js>"{name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']}"</js>
+	)
+		</p>
+		<p class='bcode w800'>
+	<jc>// Free-form</jc>
+	<ja>@Body</ja>({
+		<js>"description: 'Pet object to add to the store',"</js>,
+		<js>"required: true,"</js>,
+		<js>"example: {name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']},"</js>
+		<js>"x-extra: 'extra field'"</js>
+	})
+		</p>
+		<p class='bcode w800'>
+	<jc>// Localized</jc>
+	<ja>@Body</ja>(
+		description=<js>"$L{My.Localized.Description}"</js>,
+		required=<js>"true"</js>,
+		example=<js>"{name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']}"</js>
+	)
+		</p>
 		
 		<h5 class='section'>Other Notes:</h5>
 		<ul class='spaced-list'>
@@ -17199,6 +17235,41 @@
 		<p>
 			The Java method arguments can be annotated with any of the following:
 		</p>
+		<ul class='doctree'>
+			<li class='ja'>{@link org.apache.juneau.http.annotation.Query} - A URL query parameter.
+			<li class='ja'>{@link org.apache.juneau.http.annotation.FormData} - A form-data parameter.
+			<li class='ja'>{@link org.apache.juneau.http.annotation.Header} - A request header.
+			<li class='ja'>{@link org.apache.juneau.http.annotation.Body} - The HTTP request body.
+		</ul>
+		<p>
+			The return type of the Java method can be any of the following:
+		</p>
+		<ul class='spaced-list'>
+			<li>
+				<jk>void</jk> 
+				- Don't parse any response.  
+				<br>Note that the method will still throw a runtime exception if an error HTTP status is returned.
+			<li>
+				Any <a class='doclink' href='#juneau-marshall.PojoCategories'>parsable</a> POJO 
+				- The body of the response will be converted to the POJO using the parser defined on the 
+				<code>RestClient</code> based on the <code>Content-Type</code> of the response.
+			<li>
+				<code>HttpResponse</code> 
+				- Returns the raw <code>HttpResponse</code> returned by the inner <code>HttpClient</code>.
+			<li>
+				{@link java.io.Reader} 
+				- Returns access to the raw reader of the response.
+				<br>Note that if you don't want your response parsed as a POJO, you'll want to get the response reader 
+				directly.
+			<li>
+				{@link java.io.InputStream} 
+				- Returns access to the raw input stream of the response.
+		</ul>
+		
+		<!-- === 9.1.1 - @Body ========================================================================== -->
+		
+		<h4 class='topic' onclick='toggle(this)'><a href='#juneau-rest-client.3rdPartyProxies.Body' id='juneau-rest-client.3rdPartyProxies.Body'>9.1.1 - @Body</a></h4>
+		<div class='topic'>
 		<ul class='spaced-list'>
 			<li class='ja'>
 				{@link org.apache.juneau.http.annotation.Query} - A URL query parameter.
@@ -17254,31 +17325,190 @@
 					<li class='normal'>{@link org.apache.juneau.rest.client.NameValuePairs} 
 						- Converted to a URL-encoded FORM post.
 				</ul>
-		</ul>
-		<p>
-			The return type of the Java method can be any of the following:
-		</p>
+		</div>
+
+		<!-- === 9.1.2 - @FormData ====================================================================== -->
+		
+		<h4 class='topic' onclick='toggle(this)'><a href='#juneau-rest-client.3rdPartyProxies.FormData' id='juneau-rest-client.3rdPartyProxies.FormData'>9.1.2 - @FormData</a></h4>
+		<div class='topic'>
 		<ul class='spaced-list'>
-			<li>
-				<jk>void</jk> 
-				- Don't parse any response.  
-				<br>Note that the method will still throw a runtime exception if an error HTTP status is returned.
-			<li>
-				Any <a class='doclink' href='#juneau-marshall.PojoCategories'>parsable</a> POJO 
-				- The body of the response will be converted to the POJO using the parser defined on the 
-				<code>RestClient</code>.
-			<li>
-				<code>HttpResponse</code> 
-				- Returns the raw <code>HttpResponse</code> returned by the inner <code>HttpClient</code>.
-			<li>
-				{@link java.io.Reader} 
-				- Returns access to the raw reader of the response.
-				<br>Note that if you don't want your response parsed as a POJO, you'll want to get the response reader 
-				directly.
-			<li>
-				{@link java.io.InputStream} 
-				- Returns access to the raw input stream of the response.
-		</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Query} - A URL query parameter.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>String</code> 
+						- Treated as a query string.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.FormData} 
+				- A form-data parameter.
+				<br>Note that this is only available if the HTTP method is <code>POST</code>.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'>{@link org.apache.juneau.rest.client.NameValuePairs} 
+						- Individual name-value pairs.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Header} 
+				- A request header.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Body} 
+				- The HTTP request body.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text/bytes using the {@link org.apache.juneau.serializer.Serializer} registered 
+						with the <code>RestClient</code>.
+					<li class='normal'>{@link java.io.Reader} 
+						- Raw contents of reader will be serialized to remote resource.
+					<li class='normal'>{@link java.io.InputStream} 
+						- Raw contents of input stream will be serialized to remote resource.
+					<li class='normal'>{@link org.apache.http.HttpEntity} 
+						- Bypass Juneau serialization and pass HttpEntity directly to HttpClient.
+					<li class='normal'>{@link org.apache.juneau.rest.client.NameValuePairs} 
+						- Converted to a URL-encoded FORM post.
+				</ul>
+		</div>
+
+		<!-- === 9.1.3 - @Query ========================================================================= -->
+		
+		<h4 class='topic' onclick='toggle(this)'><a href='#juneau-rest-client.3rdPartyProxies.Query' id='juneau-rest-client.3rdPartyProxies.Query'>9.1.3 - @Query</a></h4>
+		<div class='topic'>
+		<ul class='spaced-list'>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Query} - A URL query parameter.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>String</code> 
+						- Treated as a query string.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.FormData} 
+				- A form-data parameter.
+				<br>Note that this is only available if the HTTP method is <code>POST</code>.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'>{@link org.apache.juneau.rest.client.NameValuePairs} 
+						- Individual name-value pairs.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Header} 
+				- A request header.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Body} 
+				- The HTTP request body.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text/bytes using the {@link org.apache.juneau.serializer.Serializer} registered 
+						with the <code>RestClient</code>.
+					<li class='normal'>{@link java.io.Reader} 
+						- Raw contents of reader will be serialized to remote resource.
+					<li class='normal'>{@link java.io.InputStream} 
+						- Raw contents of input stream will be serialized to remote resource.
+					<li class='normal'>{@link org.apache.http.HttpEntity} 
+						- Bypass Juneau serialization and pass HttpEntity directly to HttpClient.
+					<li class='normal'>{@link org.apache.juneau.rest.client.NameValuePairs} 
+						- Converted to a URL-encoded FORM post.
+				</ul>
+		</div>
+
+		<!-- === 9.1.4 - @Header ======================================================================== -->
+		
+		<h4 class='topic' onclick='toggle(this)'><a href='#juneau-rest-client.3rdPartyProxies.Header' id='juneau-rest-client.3rdPartyProxies.Header'>9.1.4 - @Header</a></h4>
+		<div class='topic'>
+		<ul class='spaced-list'>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Query} - A URL query parameter.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>String</code> 
+						- Treated as a query string.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.FormData} 
+				- A form-data parameter.
+				<br>Note that this is only available if the HTTP method is <code>POST</code>.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'>{@link org.apache.juneau.rest.client.NameValuePairs} 
+						- Individual name-value pairs.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Header} 
+				- A request header.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+					<li class='normal'><code>Map&lt;String,Object&gt;</code> 
+						- Individual name-value pairs.
+						<br>Values are converted to text using {@link org.apache.juneau.httppart.SimpleUonPartSerializerSession#serialize(HttpPartType,HttpPartSchema,Object)}.
+				</ul>
+			<li class='ja'>
+				{@link org.apache.juneau.http.annotation.Body} 
+				- The HTTP request body.
+				<br>The argument can be any of the following types:
+				<ul>
+					<li class='normal'>Any serializable POJO 
+						- Converted to text/bytes using the {@link org.apache.juneau.serializer.Serializer} registered 
+						with the <code>RestClient</code>.
+					<li class='normal'>{@link java.io.Reader} 
+						- Raw contents of reader will be serialized to remote resource.
+					<li class='normal'>{@link java.io.InputStream} 
+						- Raw contents of input stream will be serialized to remote resource.
+					<li class='normal'>{@link org.apache.http.HttpEntity} 
+						- Bypass Juneau serialization and pass HttpEntity directly to HttpClient.
+					<li class='normal'>{@link org.apache.juneau.rest.client.NameValuePairs} 
+						- Converted to a URL-encoded FORM post.
+				</ul>
+		</div>
 	</div>
 	
 	<!-- === 9.2 - SSL Support ========================================================================== -->
@@ -22835,6 +23065,8 @@
 		<h5 class='topic w800'>juneau-rest-client</h5>
 		<ul class='spaced-list'>
 			<li>
+				Remoteable interfaces support OpenAPI annotations.
+			<li>
 				Made improvements to the builder API for defining SSL support.
 				<br>New methods added:
 				<ul class='doctree'>
@@ -22886,6 +23118,11 @@
 					<li class='jm'>{@link org.apache.juneau.rest.widget.Widget#loadScriptWithVars(RestRequest,String) loadScriptWithVars(RestRequest,String)}
 					<li class='jm'>{@link org.apache.juneau.rest.widget.Widget#loadStyleWithVars(RestRequest,String) loadStyleWithVars(RestRequest,String)}
 				</ul>
+			<li>
+				Removed the deprecated <code>RestCall.execute()</code> method.
+				<br>Use {@link org.apache.juneau.rest.client.RestCall#run()}.
+			<li>
+				<code>RestCall.input(Object)</code> method renamed to {@link org.apache.juneau.rest.client.RestCall#body(Object)} to match OpenAPI terminology.
 		</ul>
 
 		<h5 class='topic w800'>juneau-rest-microservice</h5>
diff --git a/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/RequestBeanProxyTest.java b/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/RequestBeanProxyTest.java
index ff39f9f..7d5f064 100644
--- a/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/RequestBeanProxyTest.java
+++ b/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/RequestBeanProxyTest.java
@@ -69,7 +69,7 @@ public class RequestBeanProxyTest {
 		@Query("b") String getX1();
 		@Query(name="c") String getX2();
 		@Query @BeanProperty("d") String getX3();
-		@Query("e") String getX4();
+		@Query(name="e",allowEmptyValue=true) String getX4();
 		@Query("f") String getX5();
 		@Query("g") String getX6();
 		@Query("h") String getX7();
@@ -125,7 +125,7 @@ public class RequestBeanProxyTest {
 		public Map<String,Object> getB() {
 			return new AMap<String,Object>().append("b1","true").append("b2", "123").append("b3", "null");
 		}
-		@Query(name="*")
+		@Query(name="*",allowEmptyValue=true)
 		public Map<String,Object> getC() {
 			return new AMap<String,Object>().append("c1","v1").append("c2", 123).append("c3", null).append("c4", "");
 		}
@@ -141,17 +141,17 @@ public class RequestBeanProxyTest {
 	@Test
 	public void a02a_query_maps_plainText() throws Exception {
 		String r = a02a.normal(new A02_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a:'(a1=v1,a2=123,a3=null,a4=\\'\\')',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
 	}
 	@Test
 	public void a02b_query_maps_uon() throws Exception {
 		String r = a02b.normal(new A02_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'\\'true\\'',b2:'\\'123\\'',b3:'\\'null\\'',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a:'(a1=v1,a2=123,a3=null,a4=\\'\\')',b1:'\\'true\\'',b2:'\\'123\\'',b3:'\\'null\\'',c1:'v1',c2:'123',c4:''}", r);
 	}
 	@Test
 	public void a02c_query_maps_x() throws Exception {
 		String r = a02b.serialized(new A02_Bean());
-		assertEquals("{a1:'xv1x',a2:'x123x',a4:'xx',b1:'xtruex',b2:'x123x',b3:'xnullx',c1:'xv1x',c2:'x123x',c4:'xx'}", r);
+		assertEquals("{a:'x{a1=v1, a2=123, a3=null, a4=}x',b1:'xtruex',b2:'x123x',b3:'xnullx',c1:'xv1x',c2:'x123x',c4:'xx'}", r);
 	}
 
 	//=================================================================================================================
@@ -169,7 +169,7 @@ public class RequestBeanProxyTest {
 	}
 
 	public static class A03_Bean {
-		@Query
+		@Query(allowEmptyValue=true)
 		public NameValuePairs getA() {
 			return new NameValuePairs().append("a1","v1").append("a2", 123).append("a3", null).append("a4", "");
 		}
@@ -177,7 +177,7 @@ public class RequestBeanProxyTest {
 		public NameValuePairs getB() {
 			return new NameValuePairs().append("b1","true").append("b2", "123").append("b3", "null");
 		}
-		@Query(name="*")
+		@Query(name="*",allowEmptyValue=true)
 		public NameValuePairs getC() {
 			return new NameValuePairs().append("c1","v1").append("c2", 123).append("c3", null).append("c4", "");
 		}
@@ -198,12 +198,12 @@ public class RequestBeanProxyTest {
 	@Test
 	public void a03b_query_nameValuePairs_on() throws Exception {
 		String r = a03b.normal(new A03_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a1:'v1',a2:'\\'123\\'',a4:'',b1:'\\'true\\'',b2:'\\'123\\'',b3:'\\'null\\'',c1:'v1',c2:'\\'123\\'',c4:''}", r);
 	}
 	@Test
 	public void a03c_query_nameValuePairs_x() throws Exception {
 		String r = a03b.serialized(new A03_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a1:'xv1x',a2:'x123x',a4:'xx',b1:'xtruex',b2:'x123x',b3:'xnullx',c1:'xv1x',c2:'x123x',c4:'xx'}", r);
 	}
 
 	//=================================================================================================================
@@ -283,7 +283,7 @@ public class RequestBeanProxyTest {
 		public List<Object> getX2() {
 			return new AList<>().append("foo").append("").append("true").append("123").append("null").append(true).append(123).append(null);
 		}
-		@Query("d")
+		@Query(name="d",allowEmptyValue=true)
 		public List<Object> getX3() {
 			return new AList<>();
 		}
@@ -299,7 +299,7 @@ public class RequestBeanProxyTest {
 		public Object[] getX6() {
 			return new Object[]{"foo", "", "true", "123", "null", true, 123, null};
 		}
-		@Query("h")
+		@Query(name="h",allowEmptyValue=true)
 		public Object[] getX7() {
 			return new Object[]{};
 		}
@@ -325,7 +325,7 @@ public class RequestBeanProxyTest {
 	@Test
 	public void a06c_query_collections_x() throws Exception {
 		String r = a06b.serialized(new A06_Bean());
-		assertEquals("{a:'fooXXtrueX123XnullXtrueX123Xnull',b:'fooXXtrueX123XnullXtrueX123Xnull',c:'fooXXtrueX123XnullXtrueX123Xnull',d:'',f:'fooXXtrueX123XnullXtrueX123Xnull',g:'fooXXtrueX123XnullXtrueX123Xnull',h:''}", r);
+		assertEquals("{a:'fooXXtrueX123XnullXtrueX123Xnull',b:'fooXXtrueX123XnullXtrueX123Xnull',c:'foo||true|123|null|true|123|null',d:'',f:'fooXXtrueX123XnullXtrueX123Xnull',g:'foo||true|123|null|true|123|null',h:''}", r);
 	}
 
 	//=================================================================================================================
@@ -373,7 +373,7 @@ public class RequestBeanProxyTest {
 		public String getX3() {
 			return "d1";
 		}
-		@FormData("e")
+		@FormData(name="e",allowEmptyValue=true)
 		public String getX4() {
 			return "";
 		}
@@ -433,7 +433,7 @@ public class RequestBeanProxyTest {
 		public Map<String,Object> getB() {
 			return new AMap<String,Object>().append("b1","true").append("b2", "123").append("b3", "null");
 		}
-		@FormData(name="*")
+		@FormData(name="*",allowEmptyValue=true)
 		public Map<String,Object> getC() {
 			return new AMap<String,Object>().append("c1","v1").append("c2", 123).append("c3", null).append("c4", "");
 		}
@@ -449,17 +449,17 @@ public class RequestBeanProxyTest {
 	@Test
 	public void c02a_formData_maps_plainText() throws Exception {
 		String r = c02a.normal(new C02_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a:'(a1=v1,a2=123,a3=null,a4=\\'\\')',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
 	}
 	@Test
 	public void c02b_formData_maps_uon() throws Exception {
 		String r = c02b.normal(new C02_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'\\'true\\'',b2:'\\'123\\'',b3:'\\'null\\'',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a:'(a1=v1,a2=123,a3=null,a4=\\'\\')',b1:'\\'true\\'',b2:'\\'123\\'',b3:'\\'null\\'',c1:'v1',c2:'123',c4:''}", r);
 	}
 	@Test
 	public void c02c_formData_maps_x() throws Exception {
 		String r = c02b.serialized(new C02_Bean());
-		assertEquals("{a1:'xv1x',a2:'x123x',a4:'xx',b1:'xtruex',b2:'x123x',b3:'xnullx',c1:'xv1x',c2:'x123x',c4:'xx'}", r);
+		assertEquals("{a:'x{a1=v1, a2=123, a3=null, a4=}x',b1:'xtruex',b2:'x123x',b3:'xnullx',c1:'xv1x',c2:'x123x',c4:'xx'}", r);
 	}
 
 	//=================================================================================================================
@@ -591,7 +591,7 @@ public class RequestBeanProxyTest {
 		public List<Object> getX2() {
 			return new AList<>().append("foo").append("").append("true").append("123").append("null").append(true).append(123).append(null);
 		}
-		@FormData("d")
+		@FormData(name="d",allowEmptyValue=true)
 		public List<Object> getX3() {
 			return new AList<>();
 		}
@@ -607,7 +607,7 @@ public class RequestBeanProxyTest {
 		public Object[] getX6() {
 			return new Object[]{"foo", "", "true", "123", "null", true, 123, null};
 		}
-		@FormData("h")
+		@FormData(name="h",allowEmptyValue=true)
 		public Object[] getX7() {
 			return new Object[]{};
 		}
@@ -633,7 +633,7 @@ public class RequestBeanProxyTest {
 	@Test
 	public void c06c_formData_collections_x() throws Exception {
 		String r = c06b.serialized(new C06_Bean());
-		assertEquals("{a:'fooXXtrueX123XnullXtrueX123Xnull',b:'fooXXtrueX123XnullXtrueX123Xnull',c:'fooXXtrueX123XnullXtrueX123Xnull',d:'',f:'fooXXtrueX123XnullXtrueX123Xnull',g:'fooXXtrueX123XnullXtrueX123Xnull',h:''}", r);
+		assertEquals("{a:'fooXXtrueX123XnullXtrueX123Xnull',b:'fooXXtrueX123XnullXtrueX123Xnull',c:'foo||true|123|null|true|123|null',d:'',f:'fooXXtrueX123XnullXtrueX123Xnull',g:'foo||true|123|null|true|123|null',h:''}", r);
 	}
 
 
@@ -682,7 +682,7 @@ public class RequestBeanProxyTest {
 		public String getX3() {
 			return "d1";
 		}
-		@Header("e")
+		@Header(name="e",allowEmptyValue=true)
 		public String getX4() {
 			return "";
 		}
@@ -742,7 +742,7 @@ public class RequestBeanProxyTest {
 		public Map<String,Object> getB() {
 			return new AMap<String,Object>().append("b1","true").append("b2", "123").append("b3", "null");
 		}
-		@Header(name="*")
+		@Header(name="*",allowEmptyValue=true)
 		public Map<String,Object> getC() {
 			return new AMap<String,Object>().append("c1","v1").append("c2", 123).append("c3", null).append("c4", "");
 		}
@@ -758,17 +758,17 @@ public class RequestBeanProxyTest {
 	@Test
 	public void e02a_header_maps_plainText() throws Exception {
 		String r = e02a.normal(new E02_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a:'(a1=v1,a2=123,a3=null,a4=\\'\\')',b1:'true',b2:'123',b3:'null',c1:'v1',c2:'123',c4:''}", r);
 	}
 	@Test
 	public void e02b_header_maps_uon() throws Exception {
 		String r = e02b.normal(new E02_Bean());
-		assertEquals("{a1:'v1',a2:'123',a4:'',b1:'\\'true\\'',b2:'\\'123\\'',b3:'\\'null\\'',c1:'v1',c2:'123',c4:''}", r);
+		assertEquals("{a:'(a1=v1,a2=123,a3=null,a4=\\'\\')',b1:'\\'true\\'',b2:'\\'123\\'',b3:'\\'null\\'',c1:'v1',c2:'123',c4:''}", r);
 	}
 	@Test
 	public void e02c_header_maps_x() throws Exception {
 		String r = e02b.serialized(new E02_Bean());
-		assertEquals("{a1:'xv1x',a2:'x123x',a4:'xx',b1:'xtruex',b2:'x123x',b3:'xnullx',c1:'xv1x',c2:'x123x',c4:'xx'}", r);
+		assertEquals("{a:'x{a1=v1, a2=123, a3=null, a4=}x',b1:'xtruex',b2:'x123x',b3:'xnullx',c1:'xv1x',c2:'x123x',c4:'xx'}", r);
 	}
 
 	//=================================================================================================================
@@ -850,7 +850,7 @@ public class RequestBeanProxyTest {
 		public List<Object> getX2() {
 			return new AList<>().append("foo").append("").append("true").append("123").append("null").append(true).append(123).append(null);
 		}
-		@Header("d")
+		@Header(name="d",allowEmptyValue=true)
 		public List<Object> getX3() {
 			return new AList<>();
 		}
@@ -866,7 +866,7 @@ public class RequestBeanProxyTest {
 		public Object[] getX6() {
 			return new Object[]{"foo", "", "true", "123", "null", true, 123, null};
 		}
-		@Header("h")
+		@Header(name="h",allowEmptyValue=true)
 		public Object[] getX7() {
 			return new Object[]{};
 		}
@@ -892,7 +892,7 @@ public class RequestBeanProxyTest {
 	@Test
 	public void e04c_header_collections_x() throws Exception {
 		String r = e04b.serialized(new E04_Bean());
-		assertEquals("{a:'fooXXtrueX123XnullXtrueX123Xnull',b:'fooXXtrueX123XnullXtrueX123Xnull',c:'fooXXtrueX123XnullXtrueX123Xnull',d:'',f:'fooXXtrueX123XnullXtrueX123Xnull',g:'fooXXtrueX123XnullXtrueX123Xnull',h:''}", r);
+		assertEquals("{a:'fooXXtrueX123XnullXtrueX123Xnull',b:'fooXXtrueX123XnullXtrueX123Xnull',c:'foo||true|123|null|true|123|null',d:'',f:'fooXXtrueX123XnullXtrueX123Xnull',g:'foo||true|123|null|true|123|null',h:''}", r);
 	}
 
 	//=================================================================================================================
@@ -940,7 +940,7 @@ public class RequestBeanProxyTest {
 		public String getX3() {
 			return "d1";
 		}
-		@Path("e")
+		@Path(name="e",allowEmptyValue=true)
 		public String getX4() {
 			return "";
 		}
@@ -992,7 +992,7 @@ public class RequestBeanProxyTest {
 	}
 
 	public static class G02_Bean {
-		@Path
+		@Path(name="*",allowEmptyValue=true)
 		public Map<String,Object> getA() {
 			return new AMap<String,Object>().append("a1","v1").append("a2", 123).append("a3", null).append("a4", "");
 		}
@@ -1000,7 +1000,7 @@ public class RequestBeanProxyTest {
 		public Map<String,Object> getB() {
 			return new AMap<String,Object>().append("b1","true").append("b2", "123").append("b3", "null");
 		}
-		@Path(name="*")
+		@Path(name="*",allowEmptyValue=true)
 		public Map<String,Object> getC() {
 			return new AMap<String,Object>().append("c1","v1").append("c2", 123).append("c3", null).append("c4", "");
 		}
@@ -1044,7 +1044,7 @@ public class RequestBeanProxyTest {
 	}
 
 	public static class G03_Bean {
-		@Path
+		@Path(name="*",allowEmptyValue=true)
 		public NameValuePairs getA() {
 			return new NameValuePairs().append("a1","v1").append("a2", 123).append("a3", null).append("a4", "");
 		}
@@ -1052,7 +1052,7 @@ public class RequestBeanProxyTest {
 		public NameValuePairs getB() {
 			return new NameValuePairs().append("b1","true").append("b2", "123").append("b3", "null");
 		}
-		@Path(name="*")
+		@Path(name="*",allowEmptyValue=true)
 		public NameValuePairs getC() {
 			return new NameValuePairs().append("c1","v1").append("c2", 123).append("c3", null).append("c4", "");
 		}
@@ -1108,7 +1108,7 @@ public class RequestBeanProxyTest {
 		public List<Object> getX2() {
 			return new AList<>().append("foo").append("").append("true").append("123").append("null").append(true).append(123).append(null);
 		}
-		@Path("d")
+		@Path(name="d",allowEmptyValue=true)
 		public List<Object> getX3() {
 			return new AList<>();
 		}
@@ -1124,7 +1124,7 @@ public class RequestBeanProxyTest {
 		public Object[] getX6() {
 			return new Object[]{"foo", "", "true", "123", "null", true, 123, null};
 		}
-		@Path("h")
+		@Path(name="h",allowEmptyValue=true)
 		public Object[] getX7() {
 			return new Object[]{};
 		}
@@ -1150,7 +1150,7 @@ public class RequestBeanProxyTest {
 	@Test
 	public void g04c_path_collections_x() throws Exception {
 		String r = g04b.serialized(new G04_Bean());
-		assertEquals("echoPath/fooXXtrueX123XnullXtrueX123Xnull/fooXXtrueX123XnullXtrueX123Xnull/fooXXtrueX123XnullXtrueX123Xnull//NULL/fooXXtrueX123XnullXtrueX123Xnull/fooXXtrueX123XnullXtrueX123Xnull//NULL", r);
+		assertEquals("echoPath/fooXXtrueX123XnullXtrueX123Xnull/fooXXtrueX123XnullXtrueX123Xnull/foo||true|123|null|true|123|null//NULL/fooXXtrueX123XnullXtrueX123Xnull/foo||true|123|null|true|123|null//NULL", r);
 	}
 
 	//=================================================================================================================
diff --git a/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java b/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java
index dc9b589..2fe57e3 100644
--- a/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java
+++ b/juneau-microservice/juneau-microservice-test/src/test/java/org/apache/juneau/rest/test/client/ThirdPartyProxyTest.java
@@ -2151,7 +2151,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanPath6 {
-			@Path
+			@Path("*")
 			Map<String,Object> getX();
 		}
 
@@ -2161,7 +2161,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanPath7 {
-			@Path
+			@Path("*")
 			ABean getX();
 		}
 
@@ -2260,7 +2260,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanQuery6 {
-			@Query
+			@Query("*")
 			Map<String,Object> getX();
 		}
 
@@ -2270,7 +2270,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanQuery7 {
-			@Query
+			@Query("*")
 			ABean getX();
 		}
 
@@ -2369,7 +2369,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanFormData6 {
-			@FormData
+			@FormData("*")
 			Map<String,Object> getX();
 		}
 
@@ -2379,7 +2379,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanFormData7 {
-			@FormData
+			@FormData("*")
 			ABean getX();
 		}
 
@@ -2478,7 +2478,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanHeader6 {
-			@Header
+			@Header("*")
 			Map<String,Object> getX();
 		}
 
@@ -2488,7 +2488,7 @@ public class ThirdPartyProxyTest extends RestTestcase {
 		);
 
 		public static interface ReqBeanHeader7 {
-			@Header
+			@Header("*")
 			ABean getX();
 		}
 
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java
index 66c637d..ceb375c 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java
@@ -28,7 +28,7 @@ import org.apache.juneau.urlencoding.*;
  *
  * <p>
  * Instances of this method can be passed directly to the {@link RestClient#doPost(Object, Object)} method or
- * {@link RestCall#input(Object)} methods to perform URL-encoded form posts.
+ * {@link RestCall#body(Object)} methods to perform URL-encoded form posts.
  *
  * <h5 class='section'>Example:</h5>
  * <p class='bcode'>
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
index eb574ae..7c1e145 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
@@ -93,6 +93,7 @@ public final class RestCall extends BeanSession implements Closeable {
 	private boolean hasInput;  // input() was called, even if it's setting 'null'.
 	private Serializer serializer;
 	private Parser parser;
+	private HttpPartParser partParser;
 	private URIBuilder uriBuilder;
 	private NameValuePairs formData;
 
@@ -115,6 +116,7 @@ public final class RestCall extends BeanSession implements Closeable {
 		this.retryInterval = client.retryInterval;
 		this.serializer = client.serializer;
 		this.parser = client.parser;
+		this.partParser = client.getPartParser();
 		uriBuilder = new URIBuilder(uri);
 	}
 
@@ -207,16 +209,19 @@ public final class RestCall extends BeanSession implements Closeable {
 	public RestCall query(String name, Object value, boolean skipIfEmpty, HttpPartSerializer serializer, HttpPartSchema schema) throws RestCallException {
 		if (serializer == null)
 			serializer = client.getPartSerializer();
-		if (! ("*".equals(name) || isEmpty(name))) {
+		boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
+		if (! isMulti) {
 			if (value != null && ! (ObjectUtils.isEmpty(value) && skipIfEmpty))
 				try {
 					uriBuilder.addParameter(name, serializer.createSession(null).serialize(HttpPartType.QUERY, schema, value));
-				} catch (SerializeException | SchemaValidationException e) {
-					throw new RestCallException(e);
+				} catch (SchemaValidationException e) {
+					throw new RestCallException(e, "Validation error on request query parameter ''{0}''=''{1}''", name, value);
+				} catch (SerializeException e) {
+					throw new RestCallException(e, "Serialization error on request query parameter ''{0}''", name);
 				}
 		} else if (value instanceof NameValuePairs) {
 			for (NameValuePair p : (NameValuePairs)value)
-				query(p.getName(), p.getValue(), skipIfEmpty, SimpleUonPartSerializer.DEFAULT, schema);
+				query(p.getName(), p.getValue(), skipIfEmpty, serializer, schema);
 		} else if (value instanceof Map) {
 			for (Map.Entry<String,Object> p : ((Map<String,Object>) value).entrySet())
 				query(p.getKey(), p.getValue(), skipIfEmpty, serializer, schema);
@@ -327,7 +332,8 @@ public final class RestCall extends BeanSession implements Closeable {
 			formData = new NameValuePairs();
 		if (serializer == null)
 			serializer = client.getPartSerializer();
-		if (! ("*".equals(name) || isEmpty(name))) {
+		boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
+		if (! isMulti) {
 			if (value != null && ! (ObjectUtils.isEmpty(value) && skipIfEmpty))
 				formData.add(new SerializedNameValuePair(name, value, serializer, schema));
 		} else if (value instanceof NameValuePairs) {
@@ -341,11 +347,11 @@ public final class RestCall extends BeanSession implements Closeable {
 			return formData(name, toBeanMap(value), skipIfEmpty, serializer, schema);
 		} else if (value instanceof Reader) {
 			contentType("application/x-www-form-urlencoded");
-			input(value);
+			body(value);
 		} else if (value instanceof CharSequence) {
 			try {
 				contentType("application/x-www-form-urlencoded");
-				input(new StringEntity(value.toString()));
+				body(new StringEntity(value.toString()));
 			} catch (UnsupportedEncodingException e) {}
 		} else {
 			throw new FormattedRuntimeException("Invalid name ''{0}'' passed to formData(name,value,skipIfEmpty) for data type ''{1}''", name, getReadableClassNameForObject(value));
@@ -440,14 +446,17 @@ public final class RestCall extends BeanSession implements Closeable {
 		String path = uriBuilder.getPath();
 		if (serializer == null)
 			serializer = client.getPartSerializer();
-		if (! ("*".equals(name) || isEmpty(name))) {
+		boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
+		if (! isMulti) {
 			String var = "{" + name + "}";
 			if (path.indexOf(var) == -1)
 				throw new RestCallException("Path variable {"+name+"} was not found in path.");
 			try {
 				uriBuilder.setPath(path.replace(var, serializer.createSession(null).serialize(HttpPartType.PATH, schema, value)));
-			} catch (SerializeException | SchemaValidationException e) {
-				throw new RestCallException(e);
+			} catch (SchemaValidationException e) {
+				throw new RestCallException(e, "Validation error on request path parameter ''{0}''=''{1}''", name, value);
+			} catch (SerializeException e) {
+				throw new RestCallException(e, "Serialization error on request path parameter ''{0}''", name);
 			}
 		} else if (value instanceof NameValuePairs) {
 			for (NameValuePair p : (NameValuePairs)value)
@@ -501,11 +510,9 @@ public final class RestCall extends BeanSession implements Closeable {
 	/**
 	 * Sets the input for this REST call.
 	 *
-	 * TODO - Describe allowed input if serializer not defined.
-	 *
 	 * @param input
-	 * 	The input to be sent to the REST resource (only valid for PUT and POST) requests. <br>
-	 * 	Can be of the following types:
+	 * 	The input to be sent to the REST resource (only valid for PUT and POST) requests.
+	 * 	<br>Can be of the following types:
 	 * 	<ul class='spaced-list'>
 	 * 		<li>
 	 * 			{@link Reader} - Raw contents of {@code Reader} will be serialized to remote resource.
@@ -522,7 +529,7 @@ public final class RestCall extends BeanSession implements Closeable {
 	 * @return This object (for method chaining).
 	 * @throws RestCallException If a retry was attempted, but the entity was not repeatable.
 	 */
-	public RestCall input(final Object input) throws RestCallException {
+	public RestCall body(Object input) throws RestCallException {
 		this.input = input;
 		this.hasInput = true;
 		this.formData = null;
@@ -530,6 +537,32 @@ public final class RestCall extends BeanSession implements Closeable {
 	}
 
 	/**
+	 * Same as {@link #body(Object)} but allows you to specify a part serializer to use to serialize the body.
+	 *
+	 * @param input
+	 * 	The input to be sent to the REST resource (only valid for PUT and POST) requests. <br>
+	 * @param partSerializer
+	 * 	The part serializer to use to serialize the body of the request.
+	 * @param schema
+	 * 	The schema information about the part being serialized.
+	 * @return This object (for method chaining).
+	 * @throws RestCallException
+	 */
+	public RestCall body(Object input, HttpPartSerializer partSerializer, HttpPartSchema schema) throws RestCallException {
+		try {
+			if (partSerializer != null)
+				body(partSerializer.serialize(HttpPartType.BODY, schema, input));
+			else
+				body(input);
+		} catch (SchemaValidationException e) {
+			throw new RestCallException(e, "Validation error on request body.");
+		} catch (SerializeException e) {
+			throw new RestCallException(e, "Serialization error on request body.");
+		}
+		return this;
+	}
+
+	/**
 	 * Specifies the serializer to use on this call.
 	 *
 	 * <p>
@@ -557,7 +590,6 @@ public final class RestCall extends BeanSession implements Closeable {
 		return this;
 	}
 
-
 	//--------------------------------------------------------------------------------
 	// HTTP headers
 	//--------------------------------------------------------------------------------
@@ -584,12 +616,15 @@ public final class RestCall extends BeanSession implements Closeable {
 	public RestCall header(String name, Object value, boolean skipIfEmpty, HttpPartSerializer serializer, HttpPartSchema schema) throws RestCallException {
 		if (serializer == null)
 			serializer = client.getPartSerializer();
-		if (! ("*".equals(name) || isEmpty(name))) {
+		boolean isMulti = isEmpty(name) || "*".equals(name) || value instanceof NameValuePairs;
+		if (! isMulti) {
 			if (value != null && ! (ObjectUtils.isEmpty(value) && skipIfEmpty))
 				try {
 					request.setHeader(name, serializer.createSession(null).serialize(HttpPartType.HEADER, schema, value));
-				} catch (SerializeException | SchemaValidationException e) {
-					throw new RestCallException(e);
+				} catch (SchemaValidationException e) {
+					throw new RestCallException(e, "Validation error on request header parameter ''{0}''=''{1}''", name, value);
+				} catch (SerializeException e) {
+					throw new RestCallException(e, "Serialization error on request header parameter ''{0}''", name);
 				}
 		} else if (value instanceof NameValuePairs) {
 			for (NameValuePair p : (NameValuePairs)value)
@@ -1419,16 +1454,6 @@ public final class RestCall extends BeanSession implements Closeable {
 	}
 
 	/**
-	 * @return The HTTP response code.
-	 * @throws RestCallException
-	 * @deprecated Use {@link #run()}.
-	 */
-	@Deprecated
-	public int execute() throws RestCallException {
-		return run();
-	}
-
-	/**
 	 * Method used to execute an HTTP response where you're only interested in the HTTP response code.
 	 *
 	 * <p>
@@ -1882,7 +1907,7 @@ public final class RestCall extends BeanSession implements Closeable {
 		BeanContext bc = parser;
 		if (bc == null)
 			bc = BeanContext.DEFAULT;
-		return getResponse(bc.getClassMeta(type));
+		return getResponseInner(null, null, bc.getClassMeta(type));
 	}
 
 	/**
@@ -1975,7 +2000,37 @@ public final class RestCall extends BeanSession implements Closeable {
 		BeanContext bc = parser;
 		if (bc == null)
 			bc = BeanContext.DEFAULT;
-		return (T)getResponse(bc.getClassMeta(type, args));
+		return (T)getResponseInner(null, null, bc.getClassMeta(type, args));
+	}
+
+	/**
+	 * Same as {@link #getResponse(Type, Type...)} but allows you to specify a part parser to use for parsing the response.
+	 *
+	 * @param <T> The class type of the object to create.
+	 * @param partParser
+	 * 	The part parser.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @param schema
+	 * 	The schema information about the body of the response.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @param type
+	 * 	The object type to create.
+	 * 	<br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+	 * @param args
+	 * 	The type arguments of the class if it's a collection or map.
+	 * 	<br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+	 * 	<br>Ignored if the main type is not a map or collection.
+	 * @return The parsed object.
+	 * @throws ParseException
+	 * 	If the input contains a syntax error or is malformed, or is not valid for the specified type.
+	 * @throws IOException If a connection error occurred.
+	 * @see BeanSession#getClassMeta(Class) for argument syntax for maps and collections.
+	 */
+	public <T> T getResponse(HttpPartParser partParser, HttpPartSchema schema, Type type, Type...args) throws IOException, ParseException {
+		BeanContext bc = parser;
+		if (bc == null)
+			bc = BeanContext.DEFAULT;
+		return (T)getResponseInner(partParser, schema, bc.getClassMeta(type, args));
 	}
 
 	/**
@@ -2039,8 +2094,11 @@ public final class RestCall extends BeanSession implements Closeable {
 		return getResponsePojoRest(ObjectMap.class);
 	}
 
-	<T> T getResponse(ClassMeta<T> type) throws IOException, ParseException {
+	<T> T getResponseInner(HttpPartParser partParser, HttpPartSchema schema, ClassMeta<T> type) throws IOException, ParseException {
 		try {
+			if (partParser == null)
+				partParser = this.partParser;
+
 			Class<?> ic = type.getInnerClass();
 
 			if (ic.equals(HttpResponse.class))
@@ -2050,6 +2108,17 @@ public final class RestCall extends BeanSession implements Closeable {
 			if (ic.equals(InputStream.class))
 				return (T)getInputStream();
 
+			connect();
+			Header h = response.getFirstHeader("Content-Type");
+			MediaType mt = MediaType.forString(h == null ? null : h.getValue());
+
+			if ((isEmpty(mt) || mt.toString().equals("text/plain"))) {
+				if (type.hasStringTransform())
+					return type.getStringTransform().transform(getResponseAsString());
+				if (partParser != null)
+					return partParser.createSession(null).parse(HttpPartType.BODY, schema, getResponseAsString(), type);
+			}
+
 			if (parser != null) {
 				try (Closeable in = parser.isReaderParser() ? getReader() : getInputStream()) {
 					return parser.parse(in, type);
@@ -2062,10 +2131,6 @@ public final class RestCall extends BeanSession implements Closeable {
 			if (type.hasInputStreamTransform())
 				return type.getInputStreamTransform().transform(getInputStream());
 
-			MediaType mt = getMediaType();
-			if ((isEmpty(mt) || mt.toString().equals("text/plain")) && type.hasStringTransform())
-				return type.getStringTransform().transform(getResponseAsString());
-
 			throw new ParseException(
 				"Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}",
 				getResponseHeader("Content-Type"), parser == null ? null : parser.getMediaTypes()
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java
index 4801bc4..e5bf627 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java
@@ -94,7 +94,7 @@ public final class RestCallException extends IOException {
 	 * @throws IOException
 	 */
 	public RestCallException(String msg, HttpResponse response) throws ParseException, IOException {
-		super(format("{0}\n{1}\nstatus='{2}'\nResponse: \n{3}", msg, response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity(), UTF8)));
+		super(format("{0}\n{1}\nstatus=''{2}''\nResponse: \n{3}", msg, response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity(), UTF8)));
 	}
 
 	/**
@@ -107,7 +107,7 @@ public final class RestCallException extends IOException {
 	 * @param response The response from the server.
 	 */
 	public RestCallException(int responseCode, String responseMsg, String method, URI url, String response) {
-		super(format("HTTP method '{0}' call to '{1}' caused response code '{2},{3}'.\nResponse: \n{4}", method, url, responseCode, responseMsg, response));
+		super(format("HTTP method ''{0}'' call to ''{1}'' caused response code ''{2}, {3}''.\nResponse: \n{4}", method, url, responseCode, responseMsg, response));
 		this.responseCode = responseCode;
 		this.responseStatusMessage = responseMsg;
 		this.response = response;
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 1d44d8c..37ca742 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -14,6 +14,8 @@ package org.apache.juneau.rest.client;
 
 import static org.apache.juneau.internal.ReflectionUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.httppart.HttpPartType.*;
+import static org.apache.juneau.remoteable.ReturnValue.*;
 
 import java.io.*;
 import java.lang.reflect.*;
@@ -29,7 +31,6 @@ import org.apache.http.client.utils.*;
 import org.apache.http.entity.*;
 import org.apache.http.impl.client.*;
 import org.apache.juneau.*;
-import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.json.*;
@@ -231,13 +232,34 @@ public class RestClient extends BeanContext implements Closeable {
 	public static final String RESTCLIENT_parser = PREFIX + "parser.o";
 
 	/**
+	 * Configuration property:  Part parser.
+	 *
+	 * <h5 class='section'>Property:</h5>
+	 * <ul>
+	 * 	<li><b>Name:</b>  <js>"RestClient.partParser.o"</js>
+	 * 	<li><b>Data type:</b>  <code>Class&lt;? <jk>implements</jk> HttpPartParser&gt;</code> or {@link HttpPartParser}.
+	 * 	<li><b>Default:</b>  {@link OpenApiPartParser};
+	 * 	<li><b>Methods:</b>
+	 * 		<ul>
+	 * 			<li class='jm'>{@link RestClientBuilder#partParser(Class)}
+	 * 			<li class='jm'>{@link RestClientBuilder#partParser(HttpPartParser)}
+	 * 		</ul>
+	 * </ul>
+	 *
+	 * <h5 class='section'>Description:</h5>
+	 * <p>
+	 * The parser to use for parsing POJOs from form data, query parameters, headers, and path variables.
+	 */
+	public static final String RESTCLIENT_partParser = PREFIX + "partParser.o";
+
+	/**
 	 * Configuration property:  Part serializer.
 	 *
 	 * <h5 class='section'>Property:</h5>
 	 * <ul>
-	 * 	<li><b>Name:</b>  <js>"RestClient.urlEncodingSerializer.o"</js>
+	 * 	<li><b>Name:</b>  <js>"RestClient.partSerializer.o"</js>
 	 * 	<li><b>Data type:</b>  <code>Class&lt;? <jk>implements</jk> HttpPartSerializer&gt;</code> or {@link HttpPartSerializer}.
-	 * 	<li><b>Default:</b>  {@link SimpleUonPartSerializer};
+	 * 	<li><b>Default:</b>  {@link OpenApiPartSerializer};
 	 * 	<li><b>Methods:</b>
 	 * 		<ul>
 	 * 			<li class='jm'>{@link RestClientBuilder#partSerializer(Class)}
@@ -385,6 +407,7 @@ public class RestClient extends BeanContext implements Closeable {
 	private final boolean keepHttpClientOpen, debug;
 	private final UrlEncodingSerializer urlEncodingSerializer;  // Used for form posts only.
 	private final HttpPartSerializer partSerializer;
+	private final HttpPartParser partParser;
 	private final String rootUrl;
 	private volatile boolean isClosed = false;
 	private final StackTraceElement[] creationStack;
@@ -481,7 +504,8 @@ public class RestClient extends BeanContext implements Closeable {
 		}
 
 		this.urlEncodingSerializer = new SerializerBuilder(ps).build(UrlEncodingSerializer.class);
-		this.partSerializer = getInstanceProperty(RESTCLIENT_partSerializer, HttpPartSerializer.class, SimpleUonPartSerializer.class, true, ps);
+		this.partSerializer = getInstanceProperty(RESTCLIENT_partSerializer, HttpPartSerializer.class, OpenApiPartSerializer.class, true, ps);
+		this.partParser = getInstanceProperty(RESTCLIENT_partParser, HttpPartParser.class, OpenApiPartParser.class, true, ps);
 		this.executorService = getInstanceProperty(RESTCLIENT_executorService, ExecutorService.class, null);
 
 		RestCallInterceptor[] rci = getInstanceArrayProperty(RESTCLIENT_interceptors, RestCallInterceptor.class, new RestCallInterceptor[0]);
@@ -584,14 +608,14 @@ public class RestClient extends BeanContext implements Closeable {
 	 * @throws RestCallException If any authentication errors occurred.
 	 */
 	public RestCall doPut(Object url, Object o) throws RestCallException {
-		return doCall("PUT", url, true).input(o);
+		return doCall("PUT", url, true).body(o);
 	}
 
 	/**
 	 * Same as {@link #doPut(Object, Object)} but don't specify the input yet.
 	 *
 	 * <p>
-	 * You must call either {@link RestCall#input(Object)} or {@link RestCall#formData(String, Object)}
+	 * You must call either {@link RestCall#body(Object)} or {@link RestCall#formData(String, Object)}
 	 * to set the contents on the result object.
 	 *
 	 * @param url
@@ -636,14 +660,14 @@ public class RestClient extends BeanContext implements Closeable {
 	 * @throws RestCallException If any authentication errors occurred.
 	 */
 	public RestCall doPost(Object url, Object o) throws RestCallException {
-		return doCall("POST", url, true).input(o);
+		return doCall("POST", url, true).body(o);
 	}
 
 	/**
 	 * Same as {@link #doPost(Object, Object)} but don't specify the input yet.
 	 *
 	 * <p>
-	 * You must call either {@link RestCall#input(Object)} or {@link RestCall#formData(String, Object)} to set the
+	 * You must call either {@link RestCall#body(Object)} or {@link RestCall#formData(String, Object)} to set the
 	 * contents on the result object.
 	 *
 	 * <h5 class='section'>Notes:</h5>
@@ -710,7 +734,7 @@ public class RestClient extends BeanContext implements Closeable {
 	 */
 	public RestCall doFormPost(Object url, Object o) throws RestCallException {
 		return doCall("POST", url, true)
-			.input(o instanceof HttpEntity ? o : new RestRequestEntity(o, urlEncodingSerializer));
+			.body(o instanceof HttpEntity ? o : new RestRequestEntity(o, urlEncodingSerializer));
 	}
 
 	/**
@@ -774,7 +798,7 @@ public class RestClient extends BeanContext implements Closeable {
 			if (method != null && uri != null) {
 				rc = doCall(method, uri, content != null);
 				if (content != null)
-					rc.input(new StringEntity(content));
+					rc.body(new StringEntity(content));
 				if (h != null)
 					for (Map.Entry<String,Object> e : h.entrySet())
 						rc.header(e.getKey(), e.getValue());
@@ -818,7 +842,7 @@ public class RestClient extends BeanContext implements Closeable {
 	public RestCall doCall(HttpMethod method, Object url, Object content) throws RestCallException {
 		RestCall rc = doCall(method.name(), url, method.hasContent());
 		if (method.hasContent())
-			rc.input(content);
+			rc.body(content);
 		return rc;
 	}
 
@@ -1015,45 +1039,45 @@ public class RestClient extends BeanContext implements Closeable {
 							rc.serializer(serializer).parser(parser);
 
 							for (RemoteMethodArg a : rmm.getPathArgs())
-								rc.path(a.name, args[a.index], a.serializer, null);
+								rc.path(a.getName(), args[a.getIndex()], a.getSerializer(), a.getSchema());
 
 							for (RemoteMethodArg a : rmm.getQueryArgs())
-								rc.query(a.name, args[a.index], a.skipIfNE, a.serializer, null);
+								rc.query(a.getName(), args[a.getIndex()], a.isSkipIfEmpty(), a.getSerializer(), a.getSchema());
 
 							for (RemoteMethodArg a : rmm.getFormDataArgs())
-								rc.formData(a.name, args[a.index], a.skipIfNE, a.serializer, null);
+								rc.formData(a.getName(), args[a.getIndex()], a.isSkipIfEmpty(), a.getSerializer(), a.getSchema());
 
 							for (RemoteMethodArg a : rmm.getHeaderArgs())
-								rc.header(a.name, args[a.index], a.skipIfNE, a.serializer, null);
+								rc.header(a.getName(), args[a.getIndex()], a.isSkipIfEmpty(), a.getSerializer(), a.getSchema());
 
-							if (rmm.getBodyArg() != null)
-								rc.input(args[rmm.getBodyArg()]);
+							RemoteMethodArg ba = rmm.getBodyArg();
+							if (ba != null)
+								rc.body(args[ba.getIndex()], ba.getSerializer(), ba.getSchema());
 
 							if (rmm.getRequestBeanArgs().length > 0) {
 								BeanSession bs = createBeanSession();
-								for (RemoteMethodArg rma : rmm.getRequestBeanArgs()) {
-									BeanMap<?> bm = bs.toBeanMap(args[rma.index]);
+								for (RemoteMethodBeanArg rmba : rmm.getRequestBeanArgs()) {
+									BeanMap<?> bm = bs.toBeanMap(args[rmba.getIndex()]);
 
 									for (BeanPropertyValue bpv : bm.getValues(false)) {
 										BeanPropertyMeta pMeta = bpv.getMeta();
 										Object val = bpv.getValue();
-
-										Path p = pMeta.getAnnotation(Path.class);
-										if (p != null)
-											rc.path(getName(p.name(), p.value(), pMeta), val, getPartSerializer(p.serializer(), rma.serializer), null);
-
-										if (val != null) {
-											Query q1 = pMeta.getAnnotation(Query.class);
-											if (q1 != null)
-												rc.query(getName(q1.name(), q1.value(), pMeta), val, q1.skipIfEmpty(), getPartSerializer(q1.serializer(), rma.serializer), null);
-
-											FormData f1 = pMeta.getAnnotation(FormData.class);
-											if (f1 != null)
-												rc.formData(getName(f1.name(), f1.value(), pMeta), val, f1.skipIfEmpty(), getPartSerializer(f1.serializer(), rma.serializer), null);
-
-											org.apache.juneau.http.annotation.Header h1 = pMeta.getAnnotation(org.apache.juneau.http.annotation.Header.class);
-											if (h1 != null)
-												rc.header(getName(h1.name(), h1.value(), pMeta), val, h1.skipIfEmpty(), getPartSerializer(h1.serializer(), rma.serializer), null);
+										RemoteMethodArg a = rmba.getProperty(pMeta.getName());
+										if (a != null) {
+											HttpPartType pt = a.getPartType();
+											if (pt == PATH)
+												rc.path(a.getName(), val, ObjectUtils.firstNonNull(a.getSerializer(), rmba.getSerializer()), a.getSchema());
+											if (val != null) {
+												if (pt == QUERY) {
+													rc.query(a.getName(), val, a.isSkipIfEmpty(), ObjectUtils.firstNonNull(a.getSerializer(), rmba.getSerializer()), a.getSchema());
+												} else if (pt == FORMDATA) {
+													rc.formData(a.getName(), val, a.isSkipIfEmpty(), ObjectUtils.firstNonNull(a.getSerializer(), rmba.getSerializer()), a.getSchema());
+												} else if (pt == HEADER) {
+													rc.header(a.getName(), val, a.isSkipIfEmpty(), ObjectUtils.firstNonNull(a.getSerializer(), rmba.getSerializer()), a.getSchema());
+												} else if (pt == HttpPartType.BODY) {
+													rc.body(val, ObjectUtils.firstNonNull(a.getSerializer(), rmba.getSerializer()), a.getSchema());
+												}
+											}
 										}
 									}
 								}
@@ -1062,12 +1086,16 @@ public class RestClient extends BeanContext implements Closeable {
 							if (rmm.getOtherArgs().length > 0) {
 								Object[] otherArgs = new Object[rmm.getOtherArgs().length];
 								int i = 0;
-								for (Integer otherArg : rmm.getOtherArgs())
-									otherArgs[i++] = args[otherArg];
-								rc.input(otherArgs);
+								for (RemoteMethodArg a : rmm.getOtherArgs())
+									otherArgs[i++] = args[a.getIndex()];
+								rc.body(otherArgs);
 							}
 
-							if (rmm.getReturns() == ReturnValue.HTTP_STATUS) {
+							RemoteMethodReturn rmr = rmm.getReturns();
+							if (rmr.getReturnValue() == NONE) {
+								rc.run();
+								return null;
+							} else if (rmr.getReturnValue() == HTTP_STATUS) {
 								rc.ignoreErrors();
 								int returnCode = rc.run();
 								Class<?> rt = method.getReturnType();
@@ -1076,13 +1104,13 @@ public class RestClient extends BeanContext implements Closeable {
 								if (rt == Boolean.class || rt == boolean.class)
 									return returnCode < 400;
 								throw new RestCallException("Invalid return type on method annotated with @RemoteableMethod(returns=HTTP_STATUS).  Only integer and booleans types are valid.");
+							} else {
+								Object v = rc.getResponse(rmr.getParser(), rmr.getSchema(), method.getGenericReturnType());
+								if (v == null && method.getReturnType().isPrimitive())
+									v = ClassUtils.getPrimitiveDefault(method.getReturnType());
+								return v;
 							}
 
-							Object v = rc.getResponse(method.getGenericReturnType());
-							if (v == null && method.getReturnType().isPrimitive())
-								v = ClassUtils.getPrimitiveDefault(method.getReturnType());
-							return v;
-
 						} catch (RestCallException e) {
 							// Try to throw original exception if possible.
 							e.throwServerException(interfaceClass.getClassLoader());
@@ -1126,6 +1154,10 @@ public class RestClient extends BeanContext implements Closeable {
 		return partSerializer;
 	}
 
+	HttpPartParser getPartParser() {
+		return partParser;
+	}
+
 	URI toURI(Object url) throws URISyntaxException {
 		if (url instanceof URI)
 			return (URI)url;
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java
index 0ec6db9..4c0b45b 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java
@@ -1113,6 +1113,46 @@ public class RestClientBuilder extends BeanContextBuilder {
 	}
 
 	/**
+	 * Configuration property:  Part parser.
+	 *
+	 * <p>
+	 * The parser to use for parsing POJOs from form data, query parameters, headers, and path variables.
+	 *
+	 * <h5 class='section'>See Also:</h5>
+	 * <ul>
+	 * 	<li class='jf'>{@link RestClient#RESTCLIENT_partParser}
+	 * </ul>
+	 *
+	 * @param value
+	 * 	The new value for this setting.
+	 * 	<br>The default value is {@link OpenApiPartParser}.
+	 * @return This object (for method chaining).
+	 */
+	public RestClientBuilder partParser(Class<? extends HttpPartParser> value) {
+		return set(RESTCLIENT_partParser, value);
+	}
+
+	/**
+	 * Configuration property:  Part parser.
+	 *
+	 * <p>
+	 * Same as {@link #partParser(Class)} but takes in a parser instance.
+	 *
+	 * <h5 class='section'>See Also:</h5>
+	 * <ul>
+	 * 	<li class='jf'>{@link RestClient#RESTCLIENT_partParser}
+	 * </ul>
+	 *
+	 * @param value
+	 * 	The new value for this setting.
+	 * 	<br>The default value is {@link OpenApiPartParser}.
+	 * @return This object (for method chaining).
+	 */
+	public RestClientBuilder partParser(HttpPartParser value) {
+		return set(RESTCLIENT_partParser, value);
+	}
+
+	/**
 	 * Configuration property:  Part serializer.
 	 *
 	 * <p>
@@ -1125,7 +1165,7 @@ public class RestClientBuilder extends BeanContextBuilder {
 	 *
 	 * @param value
 	 * 	The new value for this setting.
-	 * 	<br>The default value is {@link SimpleUonPartSerializer}.
+	 * 	<br>The default value is {@link OpenApiPartSerializer}.
 	 * @return This object (for method chaining).
 	 */
 	public RestClientBuilder partSerializer(Class<? extends HttpPartSerializer> value) {
@@ -1145,7 +1185,7 @@ public class RestClientBuilder extends BeanContextBuilder {
 	 *
 	 * @param value
 	 * 	The new value for this setting.
-	 * 	<br>The default value is {@link SimpleUonPartSerializer}.
+	 * 	<br>The default value is {@link OpenApiPartSerializer}.
 	 * @return This object (for method chaining).
 	 */
 	public RestClientBuilder partSerializer(HttpPartSerializer value) {
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/SerializedNameValuePair.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/SerializedNameValuePair.java
index 5adca48..1b4fb8c 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/SerializedNameValuePair.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/SerializedNameValuePair.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.rest.client;
 
 import org.apache.http.*;
+import org.apache.juneau.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.urlencoding.*;
@@ -64,8 +65,10 @@ public final class SerializedNameValuePair implements NameValuePair {
 	public String getValue() {
 		try {
 			return serializer.createSession(null).serialize(HttpPartType.FORMDATA, schema, value);
-		} catch (SerializeException | SchemaValidationException e) {
-			throw new RuntimeException(e);
+		} catch (SchemaValidationException e) {
+			throw new FormattedRuntimeException(e, "Validation error on request form-data parameter ''{0}''=''{1}''", name, value);
+		} catch (SerializeException e) {
+			throw new FormattedRuntimeException(e, "Serialization error on request form-data parameter ''{0}''", name);
 		}
 	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestLogger.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestLogger.java
index c76d33a..918c4ea 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestLogger.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestLogger.java
@@ -52,6 +52,11 @@ public class BasicRestLogger implements RestLogger {
 		return logger;
 	}
 
+	@Override /* RestLogger */
+	public void setLevel(Level level) {
+		getLogger().setLevel(level);
+	}
+	
 	/**
 	 * Log a message to the logger.
 	 *
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index eb74228..857a332 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -26,6 +26,7 @@ import java.nio.charset.*;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.*;
+import java.util.logging.*;
 
 import javax.activation.*;
 import javax.servlet.*;
@@ -2925,6 +2926,8 @@ public final class RestContext extends BeanContext {
 			staticFileResponseHeaders = getMapProperty(REST_staticFileResponseHeaders, Object.class);
 
 			logger = getInstanceProperty(REST_logger, resource, RestLogger.class, NoOpRestLogger.class, true, this);
+			if (debug)
+				logger.setLevel(Level.FINE);
 
 			varResolver = builder.varResolverBuilder
 				.vars(
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestLogger.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestLogger.java
index ad2a4c9..0f0f074 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestLogger.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestLogger.java
@@ -40,6 +40,13 @@ public interface RestLogger {
 	public interface Null extends RestLogger {}
 
 	/**
+	 * Sets the logging level for this logger.
+	 *
+	 * @param level The new level.
+	 */
+	public void setLevel(Level level);
+
+	/**
 	 * Log a message to the logger.
 	 *
 	 * @param level The log level.