juneau-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jamesbog...@apache.org
Subject [juneau] branch master updated: Combine remoteable and servlet annotations.
Date Sun, 01 Jul 2018 19:21:52 GMT
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 2d39a18  Combine remoteable and servlet annotations.
2d39a18 is described below

commit 2d39a182592296b7e08dc8e8d5d1689ebfe28875
Author: JamesBognar <jamesbognar@apache.org>
AuthorDate: Sun Jul 1 15:21:25 2018 -0400

    Combine remoteable and servlet annotations.
---
 .../juneau/httppart/HttpPartSchemaTest_Body.java   | 1310 ++++++++++++++++++
 .../httppart/HttpPartSchemaTest_FormData.java      | 1419 ++++++++++++++++++++
 .../juneau/httppart/HttpPartSchemaTest_Header.java | 1389 +++++++++++++++++++
 .../juneau/httppart/HttpPartSchemaTest_Path.java   | 1332 ++++++++++++++++++
 .../juneau/httppart/HttpPartSchemaTest_Query.java  | 1419 ++++++++++++++++++++
 .../org/apache/juneau/testutils/TestUtils.java     |    2 +
 .../juneau/http/annotation}/AnnotationUtils.java   |  452 ++++---
 .../org/apache/juneau/http}/annotation/Body.java   |   97 +-
 .../apache/juneau/http}/annotation/Contact.java    |    2 +-
 .../juneau/http}/annotation/ExternalDocs.java      |    2 +-
 .../apache/juneau/http}/annotation/FormData.java   |  261 +++-
 .../juneau/http}/annotation/HasFormData.java       |    8 +-
 .../apache/juneau/http}/annotation/HasQuery.java   |    6 +-
 .../org/apache/juneau/http}/annotation/Header.java |  253 +++-
 .../org/apache/juneau/http}/annotation/Items.java  |    2 +-
 .../apache/juneau/http}/annotation/License.java    |    2 +-
 .../org/apache/juneau/http}/annotation/Path.java   |  218 ++-
 .../juneau/http}/annotation/PathRemainder.java     |    4 +-
 .../org/apache/juneau/http}/annotation/Query.java  |  257 +++-
 .../apache/juneau/http}/annotation/Response.java   |   18 +-
 .../juneau/http}/annotation/ResponseHeader.java    |   31 +-
 .../juneau/http}/annotation/ResponseStatus.java    |    4 +-
 .../juneau/http}/annotation/ResponseStatuses.java  |    2 +-
 .../apache/juneau/http}/annotation/Responses.java  |    2 +-
 .../org/apache/juneau/http}/annotation/Schema.java |    9 +-
 .../apache/juneau/http}/annotation/SubItems.java   |    2 +-
 .../org/apache/juneau/http}/annotation/Tag.java    |    2 +-
 .../org/apache/juneau/httppart/HttpPartParser.java |    5 +-
 .../org/apache/juneau/httppart/HttpPartSchema.java |  998 ++++++++++++--
 .../apache/juneau/httppart/HttpPartSerializer.java |    9 +-
 .../httppart/SchemaValidationParseException.java   |   22 +-
 .../SchemaValidationSerializeException.java        |   22 +-
 .../apache/juneau/httppart/SimplePartParser.java   |    3 +-
 .../juneau/httppart/oapi/OapiPartParser.java       |   12 +-
 .../apache/juneau/httppart/uon/UonPartParser.java  |    7 +-
 .../apache/juneau/internal/ReflectionUtils.java    |   32 +
 .../java/org/apache/juneau/remoteable/Body.java    |   80 --
 .../org/apache/juneau/remoteable/FormData.java     |  220 ---
 .../org/apache/juneau/remoteable/FormDataIfNE.java |   62 -
 .../java/org/apache/juneau/remoteable/Header.java  |  205 ---
 .../org/apache/juneau/remoteable/HeaderIfNE.java   |   62 -
 .../java/org/apache/juneau/remoteable/Path.java    |  192 ---
 .../java/org/apache/juneau/remoteable/Query.java   |  219 ---
 .../org/apache/juneau/remoteable/QueryIfNE.java    |   62 -
 .../juneau/remoteable/RemoteableMethodMeta.java    |   11 +-
 .../juneau/serializer/SerializeException.java      |    2 +-
 juneau-doc/src/main/javadoc/overview.html          |   12 +-
 .../juneau/examples/rest/AtomFeedResource.java     |    1 +
 .../examples/rest/CodeFormatterResource.java       |    1 +
 .../examples/rest/DockerRegistryResource.java      |    1 +
 .../juneau/examples/rest/JsonSchemaResource.java   |    1 +
 .../examples/rest/MethodExampleResource.java       |    1 +
 .../juneau/examples/rest/PhotosResource.java       |    1 +
 .../examples/rest/PredefinedLabelsResource.java    |    1 +
 .../juneau/examples/rest/RequestEchoResource.java  |    1 +
 .../examples/rest/SampleRemoteableServlet.java     |    1 +
 .../juneau/examples/rest/SqlQueryResource.java     |    3 +-
 .../examples/rest/SystemPropertiesResource.java    |    3 +-
 .../juneau/examples/rest/TempDirResource.java      |    1 +
 .../examples/rest/UrlEncodedFormResource.java      |    3 +-
 .../rest/addressbook/AddressBookResource.java      |    1 +
 .../juneau/examples/rest/petstore/IdConflict.java  |    2 +-
 .../juneau/examples/rest/petstore/IdNotFound.java  |    2 +-
 .../juneau/examples/rest/petstore/InvalidId.java   |    2 +-
 .../examples/rest/petstore/InvalidLogin.java       |    2 +-
 .../examples/rest/petstore/InvalidSpecies.java     |    2 +-
 .../juneau/examples/rest/petstore/InvalidTag.java  |    2 +-
 .../examples/rest/petstore/InvalidUsername.java    |    2 +-
 .../examples/rest/petstore/PetStoreResource.java   |   10 +-
 .../microservice/resources/ConfigResource.java     |    3 +-
 .../microservice/resources/DirectoryResource.java  |    1 +
 .../microservice/resources/LogsResource.java       |    1 +
 .../apache/juneau/rest/test/ConfigResource.java    |    1 +
 .../juneau/rest/test/LargePojosResource.java       |    1 +
 .../rest/test/client/ThirdPartyProxyResource.java  |   36 +-
 .../org/apache/juneau/rest/test/MockRestTest.java  |    1 +
 .../rest/test/client/RequestBeanProxyTest.java     | 1028 ++------------
 .../rest/test/client/ThirdPartyProxyTest.java      | 1090 +--------------
 .../org/apache/juneau/rest/client/RestCall.java    |   25 +-
 .../juneau/rest/client/RestCallException.java      |   45 +-
 .../org/apache/juneau/rest/client/RestClient.java  |   17 +-
 .../rest/client/SerializedNameValuePair.java       |    7 +-
 .../apache/juneau/rest/BasicRestCallHandler.java   |   10 +-
 .../apache/juneau/rest/BasicRestInfoProvider.java  |   35 +-
 .../java/org/apache/juneau/rest/RequestBody.java   |  172 +--
 .../org/apache/juneau/rest/RequestFormData.java    |  110 +-
 .../org/apache/juneau/rest/RequestHeaders.java     |   47 +-
 .../org/apache/juneau/rest/RequestPathMatch.java   |   52 +-
 .../java/org/apache/juneau/rest/RequestQuery.java  |  101 +-
 .../java/org/apache/juneau/rest/RestContext.java   |  129 +-
 .../org/apache/juneau/rest/RestJavaMethod.java     |    1 +
 .../org/apache/juneau/rest/RestMethodParam.java    |   20 +-
 .../org/apache/juneau/rest/RestMethodReturn.java   |   28 +-
 .../org/apache/juneau/rest/RestMethodThrown.java   |   27 +-
 .../org/apache/juneau/rest/RestParamDefaults.java  |  306 ++---
 .../java/org/apache/juneau/rest/RestRequest.java   |    3 +-
 .../juneau/rest/annotation/MethodSwagger.java      |    1 +
 .../juneau/rest/annotation/ResourceSwagger.java    |    1 +
 .../apache/juneau/rest/annotation/RestMethod.java  |    1 +
 .../apache/juneau/rest/exception/BadRequest.java   |    2 +-
 .../org/apache/juneau/rest/exception/Conflict.java |    2 +-
 .../juneau/rest/exception/ExpectationFailed.java   |    2 +-
 .../juneau/rest/exception/FailedDependency.java    |    2 +-
 .../apache/juneau/rest/exception/Forbidden.java    |    2 +-
 .../org/apache/juneau/rest/exception/Gone.java     |    2 +-
 .../rest/exception/HttpVersionNotSupported.java    |    2 +-
 .../juneau/rest/exception/InsufficientStorage.java |    2 +-
 .../juneau/rest/exception/InternalServerError.java |    2 +-
 .../juneau/rest/exception/LengthRequired.java      |    2 +-
 .../org/apache/juneau/rest/exception/Locked.java   |    2 +-
 .../apache/juneau/rest/exception/LoopDetected.java |    2 +-
 .../juneau/rest/exception/MethodNotAllowed.java    |    2 +-
 .../juneau/rest/exception/MisdirectedRequest.java  |    2 +-
 .../exception/NetworkAuthenticationRequired.java   |    2 +-
 .../juneau/rest/exception/NotAcceptable.java       |    2 +-
 .../apache/juneau/rest/exception/NotExtended.java  |    2 +-
 .../org/apache/juneau/rest/exception/NotFound.java |    2 +-
 .../juneau/rest/exception/NotImplemented.java      |    2 +-
 .../juneau/rest/exception/PayloadTooLarge.java     |    2 +-
 .../juneau/rest/exception/PreconditionFailed.java  |    2 +-
 .../rest/exception/PreconditionRequired.java       |    2 +-
 .../juneau/rest/exception/RangeNotSatisfiable.java |    2 +-
 .../exception/RequestHeaderFieldsTooLarge.java     |    2 +-
 .../juneau/rest/exception/ServiceUnavailable.java  |    2 +-
 .../juneau/rest/exception/TooManyRequests.java     |    2 +-
 .../apache/juneau/rest/exception/Unauthorized.java |    2 +-
 .../rest/exception/UnavailableForLegalReasons.java |    2 +-
 .../juneau/rest/exception/UnprocessableEntity.java |    2 +-
 .../rest/exception/UnsupportedMediaType.java       |    2 +-
 .../juneau/rest/exception/UpgradeRequired.java     |    2 +-
 .../apache/juneau/rest/exception/UriTooLong.java   |    2 +-
 .../rest/exception/VariantAlsoNegotiates.java      |    2 +-
 .../java/org/apache/juneau/rest/helper/Ok.java     |    2 +-
 .../org/apache/juneau/rest/helper/Redirect.java    |    2 +-
 .../juneau/rest/helper/RedirectToServletRoot.java  |    2 +-
 .../juneau/rest/helper/ResourceDescription.java    |    2 +-
 .../juneau/rest/remoteable/RemoteableServlet.java  |    3 +-
 .../apache/juneau/rest/util/AnnotationUtils.java   |  655 +--------
 .../juneau/rest/BasicRestInfoProviderTest.java     |    7 +-
 .../juneau/rest/BeanContextPropertiesTest.java     |    1 +
 .../java/org/apache/juneau/rest/PathsTest.java     |    1 +
 .../org/apache/juneau/rest/StatusCodesTest.java    |    7 +-
 .../juneau/rest/annotation/BodyAnnotationTest.java |    1 +
 .../rest/annotation/FormDataAnnotationTest.java    |    3 +-
 .../rest/annotation/HasFormDataAnnotationTest.java |    1 +
 .../rest/annotation/HasQueryAnnotationTest.java    |    1 +
 .../rest/annotation/HeaderAnnotationTest.java      |    1 +
 .../juneau/rest/annotation/PathAnnotationTest.java |    1 +
 .../annotation/PathRemainderAnnotationTest.java    |    1 +
 .../rest/annotation/QueryAnnotationTest.java       |    6 +-
 .../rest/annotation/ResponseAnnotationTest.java    |  105 +-
 .../annotation/ResponseHeaderAnnotationTest.java   |  356 +----
 .../annotation/ResponseStatusAnnotationTest.java   |    1 +
 .../rest/annotation/ResponsesAnnotationTest.java   |   47 +-
 .../juneau/rest/annotation/RestHookTest.java       |    1 +
 .../rest/annotation/RestMethodInheritTest.java     |    1 +
 .../rest/annotation/RestResourceParsersTest.java   |    1 +
 .../rest/annotation/RestResourcePojoSwapsTest.java |    1 +
 .../juneau/rest/headers/AcceptCharsetTest.java     |    1 +
 .../org/apache/juneau/rest/headers/AcceptTest.java |    1 +
 .../juneau/rest/headers/ContentEncodingTest.java   |    1 +
 .../juneau/rest/headers/ContentTypeTest.java       |    1 +
 .../apache/juneau/rest/headers/HeadersTest.java    |    1 +
 163 files changed, 10076 insertions(+), 5291 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
new file mode 100644
index 0000000..e83fd32
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Body.java
@@ -0,0 +1,1310 @@
+// ***************************************************************************************************************************
+// * 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.httppart;
+
+import static org.junit.Assert.*;
+
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+public class HttpPartSchemaTest_Body {
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Basic test
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testBasic() throws Exception {
+		HttpPartSchema.create().build();
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// @Body
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Body(
+		required=true,
+		description={"b1","b2"},
+		schema=@Schema($ref="c1"),
+		example="f1",
+		api="{g1:true}"
+	)
+	public static class A02 {}
+
+	@Test
+	public void a02_basic_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, A02.class).noValidate(true).build();
+		assertTrue(s.getRequired());
+		assertObjectEquals("{description:'b1\\nb2',example:'f1',required:true,schema:{'$ref':'c1'},_value:'{g1:true}'}", s.getApi());
+	}	
+
+	public static class A03 {
+		public void a(
+				@Body(
+					required=true,
+					description={"b1","b2"},
+					schema=@Schema($ref="c1"),
+					example="f1",
+					api="{g1:true}"
+				) String x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a03_basic_onParameter() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, A03.class.getMethod("a", String.class), 0).noValidate(true).build();
+		assertTrue(s.getRequired());
+		assertObjectEquals("{description:'b1\\nb2',example:'f1',required:true,schema:{'$ref':'c1'},_value:'{g1:true}'}", s.getApi());
+	}	
+	
+	public static class A04 {
+		public void a(
+				@Body(
+					required=false,
+					description={"b3","b3"},
+					schema=@Schema($ref="c3"),
+					example="f2",
+					api="{g2:true}"
+				) A02 x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a04_basic_onParameterAndClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, A04.class.getMethod("a", A02.class), 0).noValidate(true).build();
+		assertNull(s.getRequired());
+		assertObjectEquals("{description:'b3\\nb3',example:'f2',schema:{'$ref':'c3'},_value:'{g2:true}'}", s.getApi());
+	}	
+
+	@Body(
+		schema=@Schema(
+			type="number",
+			format="int32",
+			maximum="1",
+			minimum="2",
+			multipleOf="3",
+			pattern="4",
+			maxLength=1,
+			minLength=2,
+			maxItems=3,
+			minItems=4,
+			maxProperties=5,
+			minProperties=6,
+			exclusiveMaximum=true,
+			exclusiveMinimum=true,
+			uniqueItems=true,
+			_default={"c1","c2"},
+			_enum="e1,e2",
+			items=@Items(
+				type="integer",
+				format="int64",
+				collectionFormat="ssv",
+				maximum="5",
+				minimum="6",
+				multipleOf="7",
+				pattern="8",
+				maxLength=5,
+				minLength=6,
+				maxItems=7,
+				minItems=8,
+				exclusiveMaximum=false,
+				exclusiveMinimum=false,
+				uniqueItems=false,
+				_default={"c3","c4"},
+				_enum="e3,e4",
+				items=@SubItems(
+					type="string",
+					format="float",
+					collectionFormat="tsv",
+					maximum="9",
+					minimum="10",
+					multipleOf="11",
+					pattern="12",
+					maxLength=9,
+					minLength=10,
+					maxItems=11,
+					minItems=12,
+					exclusiveMaximum=true,
+					exclusiveMinimum=true,
+					uniqueItems=true,
+					_default={"c5","c6"},
+					_enum="e5,e6",
+					items={
+						"type:'array',",
+						"format:'double',",
+						"collectionFormat:'pipes',",
+						"maximum:'13',",
+						"minimum:'14',",
+						"multipleOf:'15',",
+						"pattern:'16',",
+						"maxLength:13,",
+						"minLength:14,",
+						"maxItems:15,",
+						"minItems:16,",
+						"exclusiveMaximum:false,",
+						"exclusiveMinimum:false,",
+						"uniqueItems:false,",
+						"default:'c7\\nc8',",
+						"enum:['e7','e8']",
+					}
+				)
+			)
+		)
+	)
+	public static class A05 {}
+	
+	@Test
+	public void a05_basic_nestedItems_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, A05.class).noValidate(true).build();
+
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertEquals(3, s.getMaxItems().longValue());
+		assertEquals(4, s.getMinItems().longValue());
+		assertEquals(5, s.getMaxProperties().longValue());
+		assertEquals(6, s.getMinProperties().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertTrue(s.getUniqueItems());
+		assertObjectEquals("['e1','e2']", s.getEnum());
+		assertEquals("c1\nc2", s.getDefault());
+		
+		HttpPartSchema items = s.getItems();
+		assertEquals(HttpPartSchema.Type.INTEGER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT64, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, items.getCollectionFormat());
+		assertEquals(5, items.getMaximum());
+		assertEquals(6, items.getMinimum());
+		assertEquals(7, items.getMultipleOf());
+		assertEquals("8", items.getPattern().pattern());
+		assertEquals(5, items.getMaxLength().longValue());
+		assertEquals(6, items.getMinLength().longValue());
+		assertEquals(7, items.getMaxItems().longValue());
+		assertEquals(8, items.getMinItems().longValue());
+		assertNull(items.getExclusiveMaximum());
+		assertNull(items.getExclusiveMinimum());
+		assertNull(items.getUniqueItems());
+		assertObjectEquals("['e3','e4']", items.getEnum());
+		assertEquals("c3\nc4", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.STRING, items.getType());
+		assertEquals(HttpPartSchema.Format.FLOAT, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.TSV, items.getCollectionFormat());
+		assertEquals(9, items.getMaximum());
+		assertEquals(10, items.getMinimum());
+		assertEquals(11, items.getMultipleOf());
+		assertEquals("12", items.getPattern().pattern());
+		assertEquals(9, items.getMaxLength().longValue());
+		assertEquals(10, items.getMinLength().longValue());
+		assertEquals(11, items.getMaxItems().longValue());
+		assertEquals(12, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e5','e6']", items.getEnum());
+		assertEquals("c5\nc6", items.getDefault());
+		
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.ARRAY, items.getType());
+		assertEquals(HttpPartSchema.Format.DOUBLE, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.PIPES, items.getCollectionFormat());
+		assertEquals(13, items.getMaximum());
+		assertEquals(14, items.getMinimum());
+		assertEquals(15, items.getMultipleOf());
+		assertEquals("16", items.getPattern().pattern());
+		assertEquals(13, items.getMaxLength().longValue());
+		assertEquals(14, items.getMinLength().longValue());
+		assertEquals(15, items.getMaxItems().longValue());
+		assertEquals(16, items.getMinItems().longValue());
+		assertFalse(items.getExclusiveMaximum());
+		assertFalse(items.getExclusiveMinimum());
+		assertFalse(items.getUniqueItems());
+		assertObjectEquals("['e7','e8']", items.getEnum());
+		assertEquals("c7\nc8", items.getDefault());
+
+		assertObjectEquals(
+			"{schema:{'default':'c1\\nc2','enum':['e1','e2'],exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{collectionFormat:'ssv','default':'c3\\nc4','enum':['e3','e4'],format:'int64',items:{collectionFormat:'tsv','default':'c5\\nc6','enum':['e5','e6'],exclusiveMaximum:true,exclusiveMinimum:true,format:'float',items:{type:'array',format:'double',collectionFormat:'pipes',maximum:'13',minimum:'14',multipleOf:'15',pattern:'16',maxLength:13,minLength:14,maxItems:15,minItems:16,exc [...]
+			s.getApi()
+		);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// String input validations.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Body(required=true)
+	public static class B01a {}
+	
+	@Test
+	public void b01a_required() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B01a.class).build();
+		
+		s.validateInput("x");
+		
+		try {
+			s.validateInput(null);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("No value specified.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(
+		schema=@Schema(
+			pattern="x.*"
+		)
+	)
+	public static class B02a {}
+	
+	@Test
+	public void b02a_pattern() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B02a.class).build();
+		
+		s.validateInput("x");
+		s.validateInput("xx");
+		
+		try {
+			s.validateInput("");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("y");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				pattern="w.*",
+				items=@SubItems(
+					pattern="x.*",
+					items={
+						"pattern:'y.*',",
+						"items:{pattern:'z.*'}"
+					}
+				)
+			)
+		)
+	)
+	public static class B02b {}
+	
+	@Body(
+		schema=@Schema(
+			minLength=2, maxLength=3
+		)
+	)
+	public static class B03a {}
+
+	@Test
+	public void b03a_length() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B03a.class).build();
+		s.validateInput("12");
+		s.validateInput("123");
+		s.validateInput(null);
+		try {
+			s.validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				minLength=2, maxLength=3,
+				items=@SubItems(
+					minLength=3, maxLength=4,
+					items={
+						"minLength:4,maxLength:5,",
+						"items:{minLength:5,maxLength:6}"
+					}
+				)
+			)
+		)
+	)
+	public static class B03b {}
+
+	@Test
+	public void b03b_length_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B03b.class).build();
+		
+		s.getItems().validateInput("12");
+		s.getItems().getItems().validateInput("123");
+		s.getItems().getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().getItems().validateInput("12345");
+		
+		s.getItems().validateInput("123");
+		s.getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().validateInput("12345");
+		s.getItems().getItems().getItems().getItems().validateInput("123456");
+		
+		s.getItems().validateInput(null);
+		s.getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().getItems().validateInput(null);
+		
+		try {
+			s.getItems().validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12345");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123456");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234567");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(schema=@Schema(_enum="X,Y"))
+	public static class B04a {}
+	
+	@Test
+	public void b04a_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B04a.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(schema=@Schema(_enum=" X , Y "))
+	public static class B04b {}
+	
+	@Test
+	public void b04b_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B04b.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(schema=@Schema(_enum="['X','Y']"))
+	public static class B04c {}
+	
+	@Test
+	public void b04c_enum_json() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B04c.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}	
+	
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				_enum="['W']",
+				items=@SubItems(
+					_enum="['X']",
+					items={
+						"enum:['Y'],",
+						"items:{enum:['Z']}"
+					}
+				)
+			)
+		)
+	)
+	public static class B04d {}
+	
+	@Test
+	public void b04d_enum_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, B04d.class).build();
+
+		s.getItems().validateInput("W");
+		s.getItems().getItems().validateInput("X");
+		s.getItems().getItems().getItems().validateInput("Y");
+		s.getItems().getItems().getItems().getItems().validateInput("Z");
+		
+		try {
+			s.getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['W']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Y']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Z']", e.getLocalizedMessage());
+		}
+	}	
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Numeric validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Body(schema=@Schema(minimum="10", maximum="100"))
+	public static class C01a {}
+	
+	@Test
+	public void c01a_minmax_ints() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C01a.class).build();
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(100, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				minimum="10", maximum="100",
+				items=@SubItems(
+					minimum="100", maximum="1000",
+					items={
+						"minimum:1000,maximum:10000,",
+						"items:{minimum:10000,maximum:100000}"
+					}
+				)
+			)
+		)
+	)
+	public static class C01b {}
+	
+	@Test
+	public void c01b_minmax_ints_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C01b.class).build();
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(99, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(schema=@Schema(minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true))
+	public static class C02a {}
+
+	@Test
+	public void c02a_minmax_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C02a.class).build();
+		s.validateOutput(11, BeanContext.DEFAULT);
+		s.validateOutput(99, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true,
+				items=@SubItems(
+					minimum="100", maximum="1000", exclusiveMinimum=true, exclusiveMaximum=true,
+					items={
+						"minimum:1000,maximum:10000,exclusiveMinimum:true,exclusiveMaximum:true,",
+						"items:{minimum:10000,maximum:100000,exclusiveMinimum:true,exclusiveMaximum:true}"
+					}
+				)
+			)
+		)
+	)
+	public static class C02b {}
+	
+	@Test
+	public void c02b_minmax_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C02b.class).build();
+		
+		s.getItems().validateOutput(11, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(99, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(99999, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(schema=@Schema(minimum="10.1", maximum="100.1"))
+	public static class C03a {}
+	
+	@Test
+	public void c03_minmax_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C03a.class).build();
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(100.1f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				minimum="10.1", maximum="100.1",
+				items=@SubItems(
+					minimum="100.1", maximum="1000.1",
+					items={
+						"minimum:1000.1,maximum:10000.1,",
+						"items:{minimum:10000.1,maximum:100000.1}"
+					}
+				)
+			)
+		)
+	)
+	public static class C03b {}
+	
+	@Test
+	public void c03b_minmax_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C03b.class).build();
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(schema=@Schema(minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true))
+	public static class C04a {}
+
+	@Test
+	public void c04a_minmax_floats_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C04a.class).build();
+		s.validateOutput(10.2f, BeanContext.DEFAULT);
+		s.validateOutput(100f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true,
+				items=@SubItems(
+					minimum="100.1", maximum="1000.1", exclusiveMinimum=true, exclusiveMaximum=true,
+					items={
+						"minimum:1000.1,maximum:10000.1,exclusiveMinimum:true,exclusiveMaximum:true,",
+						"items:{minimum:10000.1,maximum:100000.1,exclusiveMinimum:true,exclusiveMaximum:true}"
+					}
+				)
+			)
+		)
+	)
+	public static class C04b {}
+	
+	@Test
+	public void c04b_minmax_floats_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C04b.class).build();
+
+		s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(schema=@Schema(multipleOf="10"))
+	public static class C05a {}
+	
+	@Test
+	public void c05a_multipleOf() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C05a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(20, BeanContext.DEFAULT);
+		s.validateOutput(10f, BeanContext.DEFAULT);
+		s.validateOutput(20f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				multipleOf="10",
+				items=@SubItems(
+					multipleOf="100",
+					items={
+						"multipleOf:1000,",
+						"items:{multipleOf:10000}"
+					}
+				)
+			)
+		)
+	)
+	public static class C05b {}
+	
+	@Test
+	public void c05b_multipleOf_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C05b.class).build();
+
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(20f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Body(schema=@Schema(multipleOf="10.1"))
+	public static class C06a {}
+	
+	@Test
+	public void c06a_multipleOf_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C06a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(20.2f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				multipleOf="10.1",
+				items=@SubItems(
+					multipleOf="100.1",
+					items={
+						"multipleOf:1000.1,",
+						"items:{multipleOf:10000.1}"
+					}
+				)
+			)
+		)
+	)
+	public static class C06b {}
+	
+	@Test
+	public void c06b_multipleOf_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, C06b.class).build();
+		
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000.2f, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Collections/Array validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				uniqueItems=true,
+				items=@SubItems(
+					uniqueItems=true,
+					items={
+						"uniqueItems:true,",
+						"items:{uniqueItems:true}"
+					}
+				)
+			)
+		)
+	)
+	public static class D01 {}
+	
+	@Test
+	public void d01a_uniqueItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, D01.class).build();
+		
+		String[] good = split("a,b"), bad = split("a,a");
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Test
+	public void d01b_uniqueItems_collections() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, D01.class).build();
+		
+		AList<String> 
+			good = new AList<String>().appendAll(split("a,b")), 
+			bad = new AList<String>().appendAll(split("a,a")); 
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Body(
+		schema=@Schema(
+			items=@Items(
+				minItems=1, maxItems=2,
+				items=@SubItems(
+					minItems=2, maxItems=3,
+					items={
+						"minItems:3,maxItems:4,",
+						"items:{minItems:4,maxItems:5}"
+					}
+				)
+			)
+		)
+	)
+	public static class D02 {}
+	
+	@Test
+	public void d02a_minMaxItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Body.class, D02.class).build();
+		
+		s.getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(new String[0], BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+	}	
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_FormData.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_FormData.java
new file mode 100644
index 0000000..d6f3e2a
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_FormData.java
@@ -0,0 +1,1419 @@
+// ***************************************************************************************************************************
+// * 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.httppart;
+
+import static org.junit.Assert.*;
+
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+public class HttpPartSchemaTest_FormData {
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Basic test
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testBasic() throws Exception {
+		HttpPartSchema.create().build();
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// @FormData
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@FormData("x")
+	public static class A01 {}
+	
+	@Test
+	public void a01_value() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, A01.class).build();
+		assertEquals("x", s.getName());
+		assertObjectEquals("{}", s.getApi());
+	}
+	
+	@FormData(
+		name="x",
+		type="number",
+		format="int32",
+		collectionFormat="csv",
+		maximum="1",
+		minimum="2",
+		multipleOf="3",
+		pattern="4",
+		maxLength=1,
+		minLength=2,
+		maxItems=3,
+		minItems=4,
+		exclusiveMaximum=true,
+		exclusiveMinimum=true,
+		uniqueItems=true,
+		required=true,
+		skipIfEmpty=true,
+		description={"b1","b2"},
+		_default={"c1","c2"},
+		items=@Items($ref="d1"),
+		_enum="e1,e2,e3",
+		example="f1",
+		api="{g1:true}"
+	)
+	public static class A02 {}
+
+	@Test
+	public void a02_basic_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, A02.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertEquals(3, s.getMaxItems().longValue());
+		assertEquals(4, s.getMinItems().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertTrue(s.getUniqueItems());
+		assertTrue(s.getRequired());
+		assertTrue(s.getSkipIfEmpty());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertEquals("c1\nc2", s.getDefault());
+		assertObjectEquals("{collectionFormat:'csv','default':'c1\\nc2',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxItems:3,maxLength:1,minimum:'2',minItems:4,minLength:2,multipleOf:'3',pattern:'4',required:true,type:'number',uniqueItems:true,_value:'{g1:true}'}", s.getApi());
+	}	
+
+	public static class A03 {
+		public void a(
+				@FormData(
+					name="x",
+					type="number",
+					format="int32",
+					collectionFormat="csv",
+					maximum="1",
+					minimum="2",
+					multipleOf="3",
+					pattern="4",
+					maxLength=1,
+					minLength=2,
+					maxItems=3,
+					minItems=4,
+					exclusiveMaximum=true,
+					exclusiveMinimum=true,
+					uniqueItems=true,
+					required=true,
+					skipIfEmpty=true,
+					description={"b1","b2"},
+					_default={"c1","c2"},
+					items=@Items($ref="d1"),
+					_enum="e1,e2,e3",
+					example="f1",
+					api="{g1:true}"
+				) String x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a03_basic_onParameter() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, A03.class.getMethod("a", String.class), 0).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertEquals(3, s.getMaxItems().longValue());
+		assertEquals(4, s.getMinItems().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertTrue(s.getUniqueItems());
+		assertTrue(s.getRequired());
+		assertTrue(s.getSkipIfEmpty());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertEquals("c1\nc2", s.getDefault());
+		assertObjectEquals("{collectionFormat:'csv','default':'c1\\nc2',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxItems:3,maxLength:1,minimum:'2',minItems:4,minLength:2,multipleOf:'3',pattern:'4',required:true,type:'number',uniqueItems:true,_value:'{g1:true}'}", s.getApi());
+	}	
+	
+	public static class A04 {
+		public void a(
+				@FormData(
+					name="y",
+					type="integer",
+					format="int64",
+					collectionFormat="ssv",
+					maximum="5",
+					minimum="6",
+					multipleOf="7",
+					pattern="8",
+					maxLength=5,
+					minLength=6,
+					maxItems=7,
+					minItems=8,
+					exclusiveMaximum=false,
+					exclusiveMinimum=false,
+					uniqueItems=false,
+					required=false,
+					skipIfEmpty=false,
+					description={"b3","b3"},
+					_default={"c3","c4"},
+					items=@Items($ref="d2"),
+					_enum="e4,e5,e6",
+					example="f2",
+					api="{g2:true}"
+				) A01 x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a04_basic_onParameterAndClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, A04.class.getMethod("a", A01.class), 0).noValidate(true).build();
+		assertEquals("y", s.getName());
+		assertEquals(HttpPartSchema.Type.INTEGER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT64, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, s.getCollectionFormat());
+		assertEquals(5, s.getMaximum());
+		assertEquals(6, s.getMinimum());
+		assertEquals(7, s.getMultipleOf());
+		assertEquals("8", s.getPattern().pattern());
+		assertEquals(5, s.getMaxLength().longValue());
+		assertEquals(6, s.getMinLength().longValue());
+		assertEquals(7, s.getMaxItems().longValue());
+		assertEquals(8, s.getMinItems().longValue());
+		assertNull(s.getExclusiveMaximum());
+		assertNull(s.getExclusiveMinimum());
+		assertNull(s.getUniqueItems());
+		assertNull(s.getRequired());
+		assertNull(s.getSkipIfEmpty());
+		assertObjectEquals("['e4','e5','e6']", s.getEnum());
+		assertEquals("c3\nc4", s.getDefault());
+		assertObjectEquals("{collectionFormat:'ssv','default':'c3\\nc4',description:'b3\\nb3','enum':['e4','e5','e6'],example:'f2',format:'int64',items:{'$ref':'d2'},maximum:'5',maxItems:7,maxLength:5,minimum:'6',minItems:8,minLength:6,multipleOf:'7',pattern:'8',type:'integer',_value:'{g2:true}'}", s.getApi());
+	}	
+
+	@FormData(
+		name="x",
+		items=@Items(
+			type="number",
+			format="int32",
+			collectionFormat="csv",
+			maximum="1",
+			minimum="2",
+			multipleOf="3",
+			pattern="4",
+			maxLength=1,
+			minLength=2,
+			maxItems=3,
+			minItems=4,
+			exclusiveMaximum=true,
+			exclusiveMinimum=true,
+			uniqueItems=true,
+			_default={"c1","c2"},
+			_enum="e1,e2",
+			items=@SubItems(
+				type="integer",
+				format="int64",
+				collectionFormat="ssv",
+				maximum="5",
+				minimum="6",
+				multipleOf="7",
+				pattern="8",
+				maxLength=5,
+				minLength=6,
+				maxItems=7,
+				minItems=8,
+				exclusiveMaximum=false,
+				exclusiveMinimum=false,
+				uniqueItems=false,
+				_default={"c3","c4"},
+				_enum="e3,e4",
+				items={
+					"type:'string',",
+					"format:'float',",
+					"collectionFormat:'tsv',",
+					"maximum:'9',",
+					"minimum:'10',",
+					"multipleOf:'11',",
+					"pattern:'12',",
+					"maxLength:9,",
+					"minLength:10,",
+					"maxItems:11,",
+					"minItems:12,",
+					"exclusiveMaximum:true,",
+					"exclusiveMinimum:true,",
+					"uniqueItems:true,",
+					"default:'c5\\nc6',",
+					"enum:['e5','e6'],",
+					"items:{",
+						"type:'array',",
+						"format:'double',",
+						"collectionFormat:'pipes',",
+						"maximum:'13',",
+						"minimum:'14',",
+						"multipleOf:'15',",
+						"pattern:'16',",
+						"maxLength:13,",
+						"minLength:14,",
+						"maxItems:15,",
+						"minItems:16,",
+						"exclusiveMaximum:false,",
+						"exclusiveMinimum:false,",
+						"uniqueItems:false,",
+						"default:'c7\\nc8',",
+						"enum:['e7','e8']",
+					"}"
+				}
+			)
+		)
+	)
+	public static class A05 {}
+	
+	@Test
+	public void a05_basic_nestedItems_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, A05.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		
+		HttpPartSchema items = s.getItems();
+		assertEquals(HttpPartSchema.Type.NUMBER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT32, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, items.getCollectionFormat());
+		assertEquals(1, items.getMaximum());
+		assertEquals(2, items.getMinimum());
+		assertEquals(3, items.getMultipleOf());
+		assertEquals("4", items.getPattern().pattern());
+		assertEquals(1, items.getMaxLength().longValue());
+		assertEquals(2, items.getMinLength().longValue());
+		assertEquals(3, items.getMaxItems().longValue());
+		assertEquals(4, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e1','e2']", items.getEnum());
+		assertEquals("c1\nc2", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.INTEGER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT64, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, items.getCollectionFormat());
+		assertEquals(5, items.getMaximum());
+		assertEquals(6, items.getMinimum());
+		assertEquals(7, items.getMultipleOf());
+		assertEquals("8", items.getPattern().pattern());
+		assertEquals(5, items.getMaxLength().longValue());
+		assertEquals(6, items.getMinLength().longValue());
+		assertEquals(7, items.getMaxItems().longValue());
+		assertEquals(8, items.getMinItems().longValue());
+		assertNull(items.getExclusiveMaximum());
+		assertNull(items.getExclusiveMinimum());
+		assertNull(items.getUniqueItems());
+		assertObjectEquals("['e3','e4']", items.getEnum());
+		assertEquals("c3\nc4", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.STRING, items.getType());
+		assertEquals(HttpPartSchema.Format.FLOAT, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.TSV, items.getCollectionFormat());
+		assertEquals(9, items.getMaximum());
+		assertEquals(10, items.getMinimum());
+		assertEquals(11, items.getMultipleOf());
+		assertEquals("12", items.getPattern().pattern());
+		assertEquals(9, items.getMaxLength().longValue());
+		assertEquals(10, items.getMinLength().longValue());
+		assertEquals(11, items.getMaxItems().longValue());
+		assertEquals(12, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e5','e6']", items.getEnum());
+		assertEquals("c5\nc6", items.getDefault());
+		
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.ARRAY, items.getType());
+		assertEquals(HttpPartSchema.Format.DOUBLE, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.PIPES, items.getCollectionFormat());
+		assertEquals(13, items.getMaximum());
+		assertEquals(14, items.getMinimum());
+		assertEquals(15, items.getMultipleOf());
+		assertEquals("16", items.getPattern().pattern());
+		assertEquals(13, items.getMaxLength().longValue());
+		assertEquals(14, items.getMinLength().longValue());
+		assertEquals(15, items.getMaxItems().longValue());
+		assertEquals(16, items.getMinItems().longValue());
+		assertFalse(items.getExclusiveMaximum());
+		assertFalse(items.getExclusiveMinimum());
+		assertFalse(items.getUniqueItems());
+		assertObjectEquals("['e7','e8']", items.getEnum());
+		assertEquals("c7\nc8", items.getDefault());
+
+		assertObjectEquals(
+			"{items:{collectionFormat:'csv','default':'c1\\nc2','enum':['e1','e2'],format:'int32',exclusiveMaximum:true,exclusiveMinimum:true,items:{collectionFormat:'ssv','default':'c3\\nc4','enum':['e3','e4'],format:'int64',items:{type:'string',format:'float',collectionFormat:'tsv',maximum:'9',minimum:'10',multipleOf:'11',pattern:'12',maxLength:9,minLength:10,maxItems:11,minItems:12,exclusiveMaximum:true,exclusiveMinimum:true,uniqueItems:true,'default':'c5\\nc6','enum':['e5','e6'],items:{type:' [...]
+			s.getApi()
+		);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// String input validations.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@FormData(required=true)
+	public static class B01a {}
+	
+	@Test
+	public void b01a_required() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B01a.class).build();
+		
+		s.validateInput("x");
+		
+		try {
+			s.validateInput(null);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("No value specified.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(allowEmptyValue=true)
+	public static class B01b {}
+	
+	@Test
+	public void b01b_allowEmptyValue() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B01b.class).build();
+		
+		s.validateInput("");  
+		s.validateInput(null);
+	}
+
+	@FormData(required=true,allowEmptyValue=true)
+	public static class B01c {}
+	
+	@Test
+	public void b01b_required_allowEmptyValue() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B01c.class).build();
+		
+		s.validateInput("");  
+
+		try {
+			s.validateInput(null);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("No value specified.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(pattern="x.*")
+	public static class B02a {}
+	
+	@Test
+	public void b02a_pattern() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B02a.class).build();
+		s.validateInput("x");
+		s.validateInput("xx");
+		try {
+			s.validateInput("y");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("yx");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");  // Empty headers are never allowed.
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(minLength=2, maxLength=3)
+	public static class B03a {}
+
+	@Test
+	public void b03a_length() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B03a.class).build();
+		s.validateInput("12");
+		s.validateInput("123");
+		s.validateInput(null);
+		try {
+			s.validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(
+		items=@Items(
+			minLength=2, maxLength=3,
+			items=@SubItems(
+				minLength=3, maxLength=4,
+				items={
+					"minLength:4,maxLength:5,",
+					"items:{minLength:5,maxLength:6}"
+				}
+			)
+		)
+	)
+	public static class B03b {}
+
+	@Test
+	public void b03b_length_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B03b.class).build();
+		
+		s.getItems().validateInput("12");
+		s.getItems().getItems().validateInput("123");
+		s.getItems().getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().getItems().validateInput("12345");
+		
+		s.getItems().validateInput("123");
+		s.getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().validateInput("12345");
+		s.getItems().getItems().getItems().getItems().validateInput("123456");
+		
+		s.getItems().validateInput(null);
+		s.getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().getItems().validateInput(null);
+		
+		try {
+			s.getItems().validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12345");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123456");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234567");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(_enum="X,Y")
+	public static class B04a {}
+	
+	@Test
+	public void b04a_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B04a.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(_enum=" X , Y ")
+	public static class B04b {}
+	
+	@Test
+	public void b04b_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B04b.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(_enum="['X','Y']")
+	public static class B04c {}
+	
+	@Test
+	public void b04c_enum_json() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B04c.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}	
+	
+	@FormData(
+		items=@Items(
+			_enum="['W']",
+			items=@SubItems(
+				_enum="['X']",
+				items={
+					"enum:['Y'],",
+					"items:{enum:['Z']}"
+				}
+			)
+		)
+	)
+	public static class B04d {}
+	
+	@Test
+	public void b04d_enum_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, B04d.class).build();
+
+		s.getItems().validateInput("W");
+		s.getItems().getItems().validateInput("X");
+		s.getItems().getItems().getItems().validateInput("Y");
+		s.getItems().getItems().getItems().getItems().validateInput("Z");
+		
+		try {
+			s.getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['W']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Y']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Z']", e.getLocalizedMessage());
+		}
+	}	
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Numeric validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@FormData(minimum="10", maximum="100")
+	public static class C01a {}
+	
+	@Test
+	public void c01a_minmax_ints() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C01a.class).build();
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(100, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(
+		items=@Items(
+			minimum="10", maximum="100",
+			items=@SubItems(
+				minimum="100", maximum="1000",
+				items={
+					"minimum:1000,maximum:10000,",
+					"items:{minimum:10000,maximum:100000}"
+				}
+			)
+		)
+	)
+	public static class C01b {}
+	
+	@Test
+	public void c01b_minmax_ints_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C01b.class).build();
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(99, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C02a {}
+
+	@Test
+	public void c02a_minmax_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C02a.class).build();
+		s.validateOutput(11, BeanContext.DEFAULT);
+		s.validateOutput(99, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(
+		items=@Items(
+			minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100", maximum="1000", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000,maximum:10000,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000,maximum:100000,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C02b {}
+	
+	@Test
+	public void c02b_minmax_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C02b.class).build();
+		
+		s.getItems().validateOutput(11, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(99, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(99999, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(minimum="10.1", maximum="100.1")
+	public static class C03a {}
+	
+	@Test
+	public void c03_minmax_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C03a.class).build();
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(100.1f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(
+		items=@Items(
+			minimum="10.1", maximum="100.1",
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1",
+				items={
+					"minimum:1000.1,maximum:10000.1,",
+					"items:{minimum:10000.1,maximum:100000.1}"
+				}
+			)
+		)
+	)
+	public static class C03b {}
+	
+	@Test
+	public void c03b_minmax_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C03b.class).build();
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C04a {}
+
+	@Test
+	public void c04a_minmax_floats_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C04a.class).build();
+		s.validateOutput(10.2f, BeanContext.DEFAULT);
+		s.validateOutput(100f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(
+		items=@Items(
+			minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000.1,maximum:10000.1,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000.1,maximum:100000.1,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C04b {}
+	
+	@Test
+	public void c04b_minmax_floats_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C04b.class).build();
+
+		s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(multipleOf="10")
+	public static class C05a {}
+	
+	@Test
+	public void c05a_multipleOf() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C05a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(20, BeanContext.DEFAULT);
+		s.validateOutput(10f, BeanContext.DEFAULT);
+		s.validateOutput(20f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(
+		items=@Items(
+			multipleOf="10",
+			items=@SubItems(
+				multipleOf="100",
+				items={
+					"multipleOf:1000,",
+					"items:{multipleOf:10000}"
+				}
+			)
+		)
+	)
+	public static class C05b {}
+	
+	@Test
+	public void c05b_multipleOf_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C05b.class).build();
+
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(20f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@FormData(multipleOf="10.1")
+	public static class C06a {}
+	
+	@Test
+	public void c06a_multipleOf_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C06a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(20.2f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(
+		items=@Items(
+			multipleOf="10.1",
+			items=@SubItems(
+				multipleOf="100.1",
+				items={
+					"multipleOf:1000.1,",
+					"items:{multipleOf:10000.1}"
+				}
+			)
+		)
+	)
+	public static class C06b {}
+	
+	@Test
+	public void c06b_multipleOf_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, C06b.class).build();
+		
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000.2f, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Collections/Array validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@FormData(
+		items=@Items(
+			uniqueItems=true,
+			items=@SubItems(
+				uniqueItems=true,
+				items={
+					"uniqueItems:true,",
+					"items:{uniqueItems:true}"
+				}
+			)
+		)
+		
+	)
+	public static class D01 {}
+	
+	@Test
+	public void d01a_uniqueItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, D01.class).build();
+		
+		String[] good = split("a,b"), bad = split("a,a");
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Test
+	public void d01b_uniqueItems_collections() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, D01.class).build();
+		
+		AList<String> 
+			good = new AList<String>().appendAll(split("a,b")), 
+			bad = new AList<String>().appendAll(split("a,a")); 
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@FormData(
+		items=@Items(
+			minItems=1, maxItems=2,
+			items=@SubItems(
+				minItems=2, maxItems=3,
+				items={
+					"minItems:3,maxItems:4,",
+					"items:{minItems:4,maxItems:5}"
+				}
+			)
+		)
+		
+	)
+	public static class D02 {}
+	
+	@Test
+	public void d02a_minMaxItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(FormData.class, D02.class).build();
+		
+		s.getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(new String[0], BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+	}	
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Header.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Header.java
new file mode 100644
index 0000000..62c9dde
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Header.java
@@ -0,0 +1,1389 @@
+// ***************************************************************************************************************************
+// * 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.httppart;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+public class HttpPartSchemaTest_Header {
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Basic test
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testBasic() throws Exception {
+		HttpPartSchema.create().build();
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// @Header
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Header("x")
+	public static class A01 {}
+	
+	@Test
+	public void a01_value() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, A01.class).build();
+		assertEquals("x", s.getName());
+		assertObjectEquals("{}", s.getApi());
+	}
+	
+	@Header(
+		name="x",
+		type="number",
+		format="int32",
+		collectionFormat="csv",
+		maximum="1",
+		minimum="2",
+		multipleOf="3",
+		pattern="4",
+		maxLength=1,
+		minLength=2,
+		maxItems=3,
+		minItems=4,
+		exclusiveMaximum=true,
+		exclusiveMinimum=true,
+		uniqueItems=true,
+		required=true,
+		skipIfEmpty=true,
+		description={"b1","b2"},
+		_default={"c1","c2"},
+		items=@Items($ref="d1"),
+		_enum="e1,e2,e3",
+		example="f1",
+		api="{g1:true}"
+	)
+	public static class A02 {}
+
+	@Test
+	public void a02_basic_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, A02.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertEquals(3, s.getMaxItems().longValue());
+		assertEquals(4, s.getMinItems().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertTrue(s.getUniqueItems());
+		assertTrue(s.getRequired());
+		assertTrue(s.getSkipIfEmpty());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertEquals("c1\nc2", s.getDefault());
+		assertObjectEquals("{collectionFormat:'csv','default':'c1\\nc2',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxItems:3,maxLength:1,minimum:'2',minItems:4,minLength:2,multipleOf:'3',pattern:'4',required:true,type:'number',uniqueItems:true,_value:'{g1:true}'}", s.getApi());
+	}	
+
+	public static class A03 {
+		public void a(
+				@Header(
+					name="x",
+					type="number",
+					format="int32",
+					collectionFormat="csv",
+					maximum="1",
+					minimum="2",
+					multipleOf="3",
+					pattern="4",
+					maxLength=1,
+					minLength=2,
+					maxItems=3,
+					minItems=4,
+					exclusiveMaximum=true,
+					exclusiveMinimum=true,
+					uniqueItems=true,
+					required=true,
+					skipIfEmpty=true,
+					description={"b1","b2"},
+					_default={"c1","c2"},
+					items=@Items($ref="d1"),
+					_enum="e1,e2,e3",
+					example="f1",
+					api="{g1:true}"
+				) String x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a03_basic_onParameter() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, A03.class.getMethod("a", String.class), 0).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertEquals(3, s.getMaxItems().longValue());
+		assertEquals(4, s.getMinItems().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertTrue(s.getUniqueItems());
+		assertTrue(s.getRequired());
+		assertTrue(s.getSkipIfEmpty());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertEquals("c1\nc2", s.getDefault());
+		assertObjectEquals("{collectionFormat:'csv','default':'c1\\nc2',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxItems:3,maxLength:1,minimum:'2',minItems:4,minLength:2,multipleOf:'3',pattern:'4',required:true,type:'number',uniqueItems:true,_value:'{g1:true}'}", s.getApi());
+	}	
+	
+	public static class A04 {
+		public void a(
+				@Header(
+					name="y",
+					type="integer",
+					format="int64",
+					collectionFormat="ssv",
+					maximum="5",
+					minimum="6",
+					multipleOf="7",
+					pattern="8",
+					maxLength=5,
+					minLength=6,
+					maxItems=7,
+					minItems=8,
+					exclusiveMaximum=false,
+					exclusiveMinimum=false,
+					uniqueItems=false,
+					required=false,
+					skipIfEmpty=false,
+					description={"b3","b3"},
+					_default={"c3","c4"},
+					items=@Items($ref="d2"),
+					_enum="e4,e5,e6",
+					example="f2",
+					api="{g2:true}"
+				) A01 x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a04_basic_onParameterAndClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, A04.class.getMethod("a", A01.class), 0).noValidate(true).build();
+		assertEquals("y", s.getName());
+		assertEquals(HttpPartSchema.Type.INTEGER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT64, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, s.getCollectionFormat());
+		assertEquals(5, s.getMaximum());
+		assertEquals(6, s.getMinimum());
+		assertEquals(7, s.getMultipleOf());
+		assertEquals("8", s.getPattern().pattern());
+		assertEquals(5, s.getMaxLength().longValue());
+		assertEquals(6, s.getMinLength().longValue());
+		assertEquals(7, s.getMaxItems().longValue());
+		assertEquals(8, s.getMinItems().longValue());
+		assertNull(s.getExclusiveMaximum());
+		assertNull(s.getExclusiveMinimum());
+		assertNull(s.getUniqueItems());
+		assertNull(s.getRequired());
+		assertNull(s.getSkipIfEmpty());
+		assertObjectEquals("['e4','e5','e6']", s.getEnum());
+		assertEquals("c3\nc4", s.getDefault());
+		assertObjectEquals("{collectionFormat:'ssv','default':'c3\\nc4',description:'b3\\nb3','enum':['e4','e5','e6'],example:'f2',format:'int64',items:{'$ref':'d2'},maximum:'5',maxItems:7,maxLength:5,minimum:'6',minItems:8,minLength:6,multipleOf:'7',pattern:'8',type:'integer',_value:'{g2:true}'}", s.getApi());
+	}	
+
+	@Header(
+		name="x",
+		items=@Items(
+			type="number",
+			format="int32",
+			collectionFormat="csv",
+			maximum="1",
+			minimum="2",
+			multipleOf="3",
+			pattern="4",
+			maxLength=1,
+			minLength=2,
+			maxItems=3,
+			minItems=4,
+			exclusiveMaximum=true,
+			exclusiveMinimum=true,
+			uniqueItems=true,
+			_default={"c1","c2"},
+			_enum="e1,e2",
+			items=@SubItems(
+				type="integer",
+				format="int64",
+				collectionFormat="ssv",
+				maximum="5",
+				minimum="6",
+				multipleOf="7",
+				pattern="8",
+				maxLength=5,
+				minLength=6,
+				maxItems=7,
+				minItems=8,
+				exclusiveMaximum=false,
+				exclusiveMinimum=false,
+				uniqueItems=false,
+				_default={"c3","c4"},
+				_enum="e3,e4",
+				items={
+					"type:'string',",
+					"format:'float',",
+					"collectionFormat:'tsv',",
+					"maximum:'9',",
+					"minimum:'10',",
+					"multipleOf:'11',",
+					"pattern:'12',",
+					"maxLength:9,",
+					"minLength:10,",
+					"maxItems:11,",
+					"minItems:12,",
+					"exclusiveMaximum:true,",
+					"exclusiveMinimum:true,",
+					"uniqueItems:true,",
+					"default:'c5\\nc6',",
+					"enum:['e5','e6'],",
+					"items:{",
+						"type:'array',",
+						"format:'double',",
+						"collectionFormat:'pipes',",
+						"maximum:'13',",
+						"minimum:'14',",
+						"multipleOf:'15',",
+						"pattern:'16',",
+						"maxLength:13,",
+						"minLength:14,",
+						"maxItems:15,",
+						"minItems:16,",
+						"exclusiveMaximum:false,",
+						"exclusiveMinimum:false,",
+						"uniqueItems:false,",
+						"default:'c7\\nc8',",
+						"enum:['e7','e8']",
+					"}"
+				}
+			)
+		)
+	)
+	public static class A05 {}
+	
+	@Test
+	public void a05_basic_nestedItems_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, A05.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		
+		HttpPartSchema items = s.getItems();
+		assertEquals(HttpPartSchema.Type.NUMBER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT32, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, items.getCollectionFormat());
+		assertEquals(1, items.getMaximum());
+		assertEquals(2, items.getMinimum());
+		assertEquals(3, items.getMultipleOf());
+		assertEquals("4", items.getPattern().pattern());
+		assertEquals(1, items.getMaxLength().longValue());
+		assertEquals(2, items.getMinLength().longValue());
+		assertEquals(3, items.getMaxItems().longValue());
+		assertEquals(4, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e1','e2']", items.getEnum());
+		assertEquals("c1\nc2", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.INTEGER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT64, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, items.getCollectionFormat());
+		assertEquals(5, items.getMaximum());
+		assertEquals(6, items.getMinimum());
+		assertEquals(7, items.getMultipleOf());
+		assertEquals("8", items.getPattern().pattern());
+		assertEquals(5, items.getMaxLength().longValue());
+		assertEquals(6, items.getMinLength().longValue());
+		assertEquals(7, items.getMaxItems().longValue());
+		assertEquals(8, items.getMinItems().longValue());
+		assertNull(items.getExclusiveMaximum());
+		assertNull(items.getExclusiveMinimum());
+		assertNull(items.getUniqueItems());
+		assertObjectEquals("['e3','e4']", items.getEnum());
+		assertEquals("c3\nc4", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.STRING, items.getType());
+		assertEquals(HttpPartSchema.Format.FLOAT, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.TSV, items.getCollectionFormat());
+		assertEquals(9, items.getMaximum());
+		assertEquals(10, items.getMinimum());
+		assertEquals(11, items.getMultipleOf());
+		assertEquals("12", items.getPattern().pattern());
+		assertEquals(9, items.getMaxLength().longValue());
+		assertEquals(10, items.getMinLength().longValue());
+		assertEquals(11, items.getMaxItems().longValue());
+		assertEquals(12, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e5','e6']", items.getEnum());
+		assertEquals("c5\nc6", items.getDefault());
+		
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.ARRAY, items.getType());
+		assertEquals(HttpPartSchema.Format.DOUBLE, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.PIPES, items.getCollectionFormat());
+		assertEquals(13, items.getMaximum());
+		assertEquals(14, items.getMinimum());
+		assertEquals(15, items.getMultipleOf());
+		assertEquals("16", items.getPattern().pattern());
+		assertEquals(13, items.getMaxLength().longValue());
+		assertEquals(14, items.getMinLength().longValue());
+		assertEquals(15, items.getMaxItems().longValue());
+		assertEquals(16, items.getMinItems().longValue());
+		assertFalse(items.getExclusiveMaximum());
+		assertFalse(items.getExclusiveMinimum());
+		assertFalse(items.getUniqueItems());
+		assertObjectEquals("['e7','e8']", items.getEnum());
+		assertEquals("c7\nc8", items.getDefault());
+
+		assertObjectEquals(
+			"{items:{collectionFormat:'csv','default':'c1\\nc2','enum':['e1','e2'],format:'int32',exclusiveMaximum:true,exclusiveMinimum:true,items:{collectionFormat:'ssv','default':'c3\\nc4','enum':['e3','e4'],format:'int64',items:{type:'string',format:'float',collectionFormat:'tsv',maximum:'9',minimum:'10',multipleOf:'11',pattern:'12',maxLength:9,minLength:10,maxItems:11,minItems:12,exclusiveMaximum:true,exclusiveMinimum:true,uniqueItems:true,'default':'c5\\nc6','enum':['e5','e6'],items:{type:' [...]
+			s.getApi()
+		);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// String input validations.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Header(required=true)
+	public static class B01 {}
+	
+	@Test
+	public void b01_required() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B01.class).build();
+		s.validateInput("x");
+		try {
+			s.validateInput(null);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("No value specified.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");  // Empty headers are never allowed.
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(pattern="x.*")
+	public static class B02a {}
+	
+	@Test
+	public void b02a_pattern() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B02a.class).build();
+		s.validateInput("x");
+		s.validateInput("xx");
+		try {
+			s.validateInput("y");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("yx");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");  // Empty headers are never allowed.
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(minLength=2, maxLength=3)
+	public static class B03a {}
+
+	@Test
+	public void b03a_length() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B03a.class).build();
+		s.validateInput("12");
+		s.validateInput("123");
+		s.validateInput(null);
+		try {
+			s.validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(
+		items=@Items(
+			minLength=2, maxLength=3,
+			items=@SubItems(
+				minLength=3, maxLength=4,
+				items={
+					"minLength:4,maxLength:5,",
+					"items:{minLength:5,maxLength:6}"
+				}
+			)
+		)
+	)
+	public static class B03b {}
+
+	@Test
+	public void b03b_length_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B03b.class).build();
+		
+		s.getItems().validateInput("12");
+		s.getItems().getItems().validateInput("123");
+		s.getItems().getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().getItems().validateInput("12345");
+		
+		s.getItems().validateInput("123");
+		s.getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().validateInput("12345");
+		s.getItems().getItems().getItems().getItems().validateInput("123456");
+		
+		s.getItems().validateInput(null);
+		s.getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().getItems().validateInput(null);
+		
+		try {
+			s.getItems().validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12345");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123456");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234567");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(_enum="X,Y")
+	public static class B04a {}
+	
+	@Test
+	public void b04a_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B04a.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(_enum=" X , Y ")
+	public static class B04b {}
+	
+	@Test
+	public void b04b_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B04b.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(_enum="['X','Y']")
+	public static class B04c {}
+	
+	@Test
+	public void b04c_enum_json() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B04c.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}	
+	
+	@Header(
+		items=@Items(
+			_enum="['W']",
+			items=@SubItems(
+				_enum="['X']",
+				items={
+					"enum:['Y'],",
+					"items:{enum:['Z']}"
+				}
+			)
+		)
+	)
+	public static class B04d {}
+	
+	@Test
+	public void b04d_enum_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, B04d.class).build();
+
+		s.getItems().validateInput("W");
+		s.getItems().getItems().validateInput("X");
+		s.getItems().getItems().getItems().validateInput("Y");
+		s.getItems().getItems().getItems().getItems().validateInput("Z");
+		
+		try {
+			s.getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['W']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Y']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Z']", e.getLocalizedMessage());
+		}
+	}	
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Numeric validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Header(minimum="10", maximum="100")
+	public static class C01a {}
+	
+	@Test
+	public void c01a_minmax_ints() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C01a.class).build();
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(100, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(
+		items=@Items(
+			minimum="10", maximum="100",
+			items=@SubItems(
+				minimum="100", maximum="1000",
+				items={
+					"minimum:1000,maximum:10000,",
+					"items:{minimum:10000,maximum:100000}"
+				}
+			)
+		)
+	)
+	public static class C01b {}
+	
+	@Test
+	public void c01b_minmax_ints_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C01b.class).build();
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(99, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C02a {}
+
+	@Test
+	public void c02a_minmax_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C02a.class).build();
+		s.validateOutput(11, BeanContext.DEFAULT);
+		s.validateOutput(99, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(
+		items=@Items(
+			minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100", maximum="1000", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000,maximum:10000,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000,maximum:100000,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C02b {}
+	
+	@Test
+	public void c02b_minmax_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C02b.class).build();
+		
+		s.getItems().validateOutput(11, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(99, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(99999, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(minimum="10.1", maximum="100.1")
+	public static class C03a {}
+	
+	@Test
+	public void c03_minmax_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C03a.class).build();
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(100.1f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(
+		items=@Items(
+			minimum="10.1", maximum="100.1",
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1",
+				items={
+					"minimum:1000.1,maximum:10000.1,",
+					"items:{minimum:10000.1,maximum:100000.1}"
+				}
+			)
+		)
+	)
+	public static class C03b {}
+	
+	@Test
+	public void c03b_minmax_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C03b.class).build();
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C04a {}
+
+	@Test
+	public void c04a_minmax_floats_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C04a.class).build();
+		s.validateOutput(10.2f, BeanContext.DEFAULT);
+		s.validateOutput(100f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(
+		items=@Items(
+			minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000.1,maximum:10000.1,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000.1,maximum:100000.1,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C04b {}
+	
+	@Test
+	public void c04b_minmax_floats_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C04b.class).build();
+
+		s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(multipleOf="10")
+	public static class C05a {}
+	
+	@Test
+	public void c05a_multipleOf() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C05a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(20, BeanContext.DEFAULT);
+		s.validateOutput(10f, BeanContext.DEFAULT);
+		s.validateOutput(20f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(
+		items=@Items(
+			multipleOf="10",
+			items=@SubItems(
+				multipleOf="100",
+				items={
+					"multipleOf:1000,",
+					"items:{multipleOf:10000}"
+				}
+			)
+		)
+	)
+	public static class C05b {}
+	
+	@Test
+	public void c05b_multipleOf_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C05b.class).build();
+
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(20f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Header(multipleOf="10.1")
+	public static class C06a {}
+	
+	@Test
+	public void c06a_multipleOf_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C06a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(20.2f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(
+		items=@Items(
+			multipleOf="10.1",
+			items=@SubItems(
+				multipleOf="100.1",
+				items={
+					"multipleOf:1000.1,",
+					"items:{multipleOf:10000.1}"
+				}
+			)
+		)
+	)
+	public static class C06b {}
+	
+	@Test
+	public void c06b_multipleOf_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, C06b.class).build();
+		
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000.2f, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Collections/Array validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Header(
+		items=@Items(
+			uniqueItems=true,
+			items=@SubItems(
+				uniqueItems=true,
+				items={
+					"uniqueItems:true,",
+					"items:{uniqueItems:true}"
+				}
+			)
+		)
+		
+	)
+	public static class D01 {}
+	
+	@Test
+	public void d01a_uniqueItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, D01.class).build();
+		
+		String[] good = split("a,b"), bad = split("a,a");
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Test
+	public void d01b_uniqueItems_collections() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, D01.class).build();
+		
+		AList<String> 
+			good = new AList<String>().appendAll(split("a,b")), 
+			bad = new AList<String>().appendAll(split("a,a")); 
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Header(
+		items=@Items(
+			minItems=1, maxItems=2,
+			items=@SubItems(
+				minItems=2, maxItems=3,
+				items={
+					"minItems:3,maxItems:4,",
+					"items:{minItems:4,maxItems:5}"
+				}
+			)
+		)
+		
+	)
+	public static class D02 {}
+	
+	@Test
+	public void d02a_minMaxItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Header.class, D02.class).build();
+		
+		s.getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(new String[0], BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+	}	
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Path.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Path.java
new file mode 100644
index 0000000..f88f8ee
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Path.java
@@ -0,0 +1,1332 @@
+// ***************************************************************************************************************************
+// * 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.httppart;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+public class HttpPartSchemaTest_Path {
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Basic test
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testBasic() throws Exception {
+		HttpPartSchema.create().build();
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// @Path
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Path("x")
+	public static class A01 {}
+	
+	@Test
+	public void a01_value() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, A01.class).build();
+		assertEquals("x", s.getName());
+		assertObjectEquals("{}", s.getApi());
+	}
+	
+	@Path(
+		name="x",
+		type="number",
+		format="int32",
+		collectionFormat="csv",
+		maximum="1",
+		minimum="2",
+		multipleOf="3",
+		pattern="4",
+		maxLength=1,
+		minLength=2,
+		exclusiveMaximum=true,
+		exclusiveMinimum=true,
+		description={"b1","b2"},
+		items=@Items($ref="d1"),
+		_enum="e1,e2,e3",
+		example="f1",
+		api="{g1:true}"
+	)
+	public static class A02 {}
+
+	@Test
+	public void a02_basic_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, A02.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertObjectEquals("{collectionFormat:'csv',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxLength:1,minimum:'2',minLength:2,multipleOf:'3',pattern:'4',type:'number',_value:'{g1:true}'}", s.getApi());
+	}	
+
+	public static class A03 {
+		public void a(
+				@Path(
+					name="x",
+					type="number",
+					format="int32",
+					collectionFormat="csv",
+					maximum="1",
+					minimum="2",
+					multipleOf="3",
+					pattern="4",
+					maxLength=1,
+					minLength=2,
+					exclusiveMaximum=true,
+					exclusiveMinimum=true,
+					description={"b1","b2"},
+					items=@Items($ref="d1"),
+					_enum="e1,e2,e3",
+					example="f1",
+					api="{g1:true}"
+				) String x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a03_basic_onParameter() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, A03.class.getMethod("a", String.class), 0).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertObjectEquals("{collectionFormat:'csv',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxLength:1,minimum:'2',minLength:2,multipleOf:'3',pattern:'4',type:'number',_value:'{g1:true}'}", s.getApi());
+	}	
+	
+	public static class A04 {
+		public void a(
+				@Path(
+					name="y",
+					type="integer",
+					format="int64",
+					collectionFormat="ssv",
+					maximum="5",
+					minimum="6",
+					multipleOf="7",
+					pattern="8",
+					maxLength=5,
+					minLength=6,
+					exclusiveMaximum=false,
+					exclusiveMinimum=false,
+					description={"b3","b3"},
+					items=@Items($ref="d2"),
+					_enum="e4,e5,e6",
+					example="f2",
+					api="{g2:true}"
+				) A01 x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a04_basic_onParameterAndClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, A04.class.getMethod("a", A01.class), 0).noValidate(true).build();
+		assertEquals("y", s.getName());
+		assertEquals(HttpPartSchema.Type.INTEGER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT64, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, s.getCollectionFormat());
+		assertEquals(5, s.getMaximum());
+		assertEquals(6, s.getMinimum());
+		assertEquals(7, s.getMultipleOf());
+		assertEquals("8", s.getPattern().pattern());
+		assertEquals(5, s.getMaxLength().longValue());
+		assertEquals(6, s.getMinLength().longValue());
+		assertNull(s.getExclusiveMaximum());
+		assertNull(s.getExclusiveMinimum());
+		assertObjectEquals("['e4','e5','e6']", s.getEnum());
+		assertObjectEquals("{collectionFormat:'ssv',description:'b3\\nb3','enum':['e4','e5','e6'],example:'f2',format:'int64',items:{'$ref':'d2'},maximum:'5',maxLength:5,minimum:'6',minLength:6,multipleOf:'7',pattern:'8',type:'integer',_value:'{g2:true}'}", s.getApi());
+	}	
+
+	@Path(
+		name="x",
+		items=@Items(
+			type="number",
+			format="int32",
+			collectionFormat="csv",
+			maximum="1",
+			minimum="2",
+			multipleOf="3",
+			pattern="4",
+			maxLength=1,
+			minLength=2,
+			maxItems=3,
+			minItems=4,
+			exclusiveMaximum=true,
+			exclusiveMinimum=true,
+			uniqueItems=true,
+			_default={"c1","c2"},
+			_enum="e1,e2",
+			items=@SubItems(
+				type="integer",
+				format="int64",
+				collectionFormat="ssv",
+				maximum="5",
+				minimum="6",
+				multipleOf="7",
+				pattern="8",
+				maxLength=5,
+				minLength=6,
+				maxItems=7,
+				minItems=8,
+				exclusiveMaximum=false,
+				exclusiveMinimum=false,
+				uniqueItems=false,
+				_default={"c3","c4"},
+				_enum="e3,e4",
+				items={
+					"type:'string',",
+					"format:'float',",
+					"collectionFormat:'tsv',",
+					"maximum:'9',",
+					"minimum:'10',",
+					"multipleOf:'11',",
+					"pattern:'12',",
+					"maxLength:9,",
+					"minLength:10,",
+					"maxItems:11,",
+					"minItems:12,",
+					"exclusiveMaximum:true,",
+					"exclusiveMinimum:true,",
+					"uniqueItems:true,",
+					"default:'c5\\nc6',",
+					"enum:['e5','e6'],",
+					"items:{",
+						"type:'array',",
+						"format:'double',",
+						"collectionFormat:'pipes',",
+						"maximum:'13',",
+						"minimum:'14',",
+						"multipleOf:'15',",
+						"pattern:'16',",
+						"maxLength:13,",
+						"minLength:14,",
+						"maxItems:15,",
+						"minItems:16,",
+						"exclusiveMaximum:false,",
+						"exclusiveMinimum:false,",
+						"uniqueItems:false,",
+						"default:'c7\\nc8',",
+						"enum:['e7','e8']",
+					"}"
+				}
+			)
+		)
+	)
+	public static class A05 {}
+	
+	@Test
+	public void a05_basic_nestedItems_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, A05.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		
+		HttpPartSchema items = s.getItems();
+		assertEquals(HttpPartSchema.Type.NUMBER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT32, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, items.getCollectionFormat());
+		assertEquals(1, items.getMaximum());
+		assertEquals(2, items.getMinimum());
+		assertEquals(3, items.getMultipleOf());
+		assertEquals("4", items.getPattern().pattern());
+		assertEquals(1, items.getMaxLength().longValue());
+		assertEquals(2, items.getMinLength().longValue());
+		assertEquals(3, items.getMaxItems().longValue());
+		assertEquals(4, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e1','e2']", items.getEnum());
+		assertEquals("c1\nc2", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.INTEGER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT64, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, items.getCollectionFormat());
+		assertEquals(5, items.getMaximum());
+		assertEquals(6, items.getMinimum());
+		assertEquals(7, items.getMultipleOf());
+		assertEquals("8", items.getPattern().pattern());
+		assertEquals(5, items.getMaxLength().longValue());
+		assertEquals(6, items.getMinLength().longValue());
+		assertEquals(7, items.getMaxItems().longValue());
+		assertEquals(8, items.getMinItems().longValue());
+		assertNull(items.getExclusiveMaximum());
+		assertNull(items.getExclusiveMinimum());
+		assertNull(items.getUniqueItems());
+		assertObjectEquals("['e3','e4']", items.getEnum());
+		assertEquals("c3\nc4", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.STRING, items.getType());
+		assertEquals(HttpPartSchema.Format.FLOAT, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.TSV, items.getCollectionFormat());
+		assertEquals(9, items.getMaximum());
+		assertEquals(10, items.getMinimum());
+		assertEquals(11, items.getMultipleOf());
+		assertEquals("12", items.getPattern().pattern());
+		assertEquals(9, items.getMaxLength().longValue());
+		assertEquals(10, items.getMinLength().longValue());
+		assertEquals(11, items.getMaxItems().longValue());
+		assertEquals(12, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e5','e6']", items.getEnum());
+		assertEquals("c5\nc6", items.getDefault());
+		
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.ARRAY, items.getType());
+		assertEquals(HttpPartSchema.Format.DOUBLE, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.PIPES, items.getCollectionFormat());
+		assertEquals(13, items.getMaximum());
+		assertEquals(14, items.getMinimum());
+		assertEquals(15, items.getMultipleOf());
+		assertEquals("16", items.getPattern().pattern());
+		assertEquals(13, items.getMaxLength().longValue());
+		assertEquals(14, items.getMinLength().longValue());
+		assertEquals(15, items.getMaxItems().longValue());
+		assertEquals(16, items.getMinItems().longValue());
+		assertFalse(items.getExclusiveMaximum());
+		assertFalse(items.getExclusiveMinimum());
+		assertFalse(items.getUniqueItems());
+		assertObjectEquals("['e7','e8']", items.getEnum());
+		assertEquals("c7\nc8", items.getDefault());
+
+		assertObjectEquals(
+			"{items:{collectionFormat:'csv','default':'c1\\nc2','enum':['e1','e2'],format:'int32',exclusiveMaximum:true,exclusiveMinimum:true,items:{collectionFormat:'ssv','default':'c3\\nc4','enum':['e3','e4'],format:'int64',items:{type:'string',format:'float',collectionFormat:'tsv',maximum:'9',minimum:'10',multipleOf:'11',pattern:'12',maxLength:9,minLength:10,maxItems:11,minItems:12,exclusiveMaximum:true,exclusiveMinimum:true,uniqueItems:true,'default':'c5\\nc6','enum':['e5','e6'],items:{type:' [...]
+			s.getApi()
+		);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// String input validations.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Path(pattern="x.*")
+	public static class B02a {}
+	
+	@Test
+	public void b02a_pattern() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, B02a.class).build();
+		s.validateInput("x");
+		s.validateInput("xx");
+		try {
+			s.validateInput("y");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("yx");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");  // Empty headers are never allowed.
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(minLength=2, maxLength=3)
+	public static class B03a {}
+
+	@Test
+	public void b03a_length() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, B03a.class).build();
+		s.validateInput("12");
+		s.validateInput("123");
+		s.validateInput(null);
+		try {
+			s.validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(
+		items=@Items(
+			minLength=2, maxLength=3,
+			items=@SubItems(
+				minLength=3, maxLength=4,
+				items={
+					"minLength:4,maxLength:5,",
+					"items:{minLength:5,maxLength:6}"
+				}
+			)
+		)
+	)
+	public static class B03b {}
+
+	@Test
+	public void b03b_length_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, B03b.class).build();
+		
+		s.getItems().validateInput("12");
+		s.getItems().getItems().validateInput("123");
+		s.getItems().getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().getItems().validateInput("12345");
+		
+		s.getItems().validateInput("123");
+		s.getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().validateInput("12345");
+		s.getItems().getItems().getItems().getItems().validateInput("123456");
+		
+		s.getItems().validateInput(null);
+		s.getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().getItems().validateInput(null);
+		
+		try {
+			s.getItems().validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12345");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123456");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234567");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(_enum="X,Y")
+	public static class B04a {}
+	
+	@Test
+	public void b04a_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, B04a.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(_enum=" X , Y ")
+	public static class B04b {}
+	
+	@Test
+	public void b04b_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, B04b.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(_enum="['X','Y']")
+	public static class B04c {}
+	
+	@Test
+	public void b04c_enum_json() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, B04c.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}	
+	
+	@Path(
+		items=@Items(
+			_enum="['W']",
+			items=@SubItems(
+				_enum="['X']",
+				items={
+					"enum:['Y'],",
+					"items:{enum:['Z']}"
+				}
+			)
+		)
+	)
+	public static class B04d {}
+	
+	@Test
+	public void b04d_enum_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, B04d.class).build();
+
+		s.getItems().validateInput("W");
+		s.getItems().getItems().validateInput("X");
+		s.getItems().getItems().getItems().validateInput("Y");
+		s.getItems().getItems().getItems().getItems().validateInput("Z");
+		
+		try {
+			s.getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['W']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Y']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Z']", e.getLocalizedMessage());
+		}
+	}	
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Numeric validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Path(minimum="10", maximum="100")
+	public static class C01a {}
+	
+	@Test
+	public void c01a_minmax_ints() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C01a.class).build();
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(100, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(
+		items=@Items(
+			minimum="10", maximum="100",
+			items=@SubItems(
+				minimum="100", maximum="1000",
+				items={
+					"minimum:1000,maximum:10000,",
+					"items:{minimum:10000,maximum:100000}"
+				}
+			)
+		)
+	)
+	public static class C01b {}
+	
+	@Test
+	public void c01b_minmax_ints_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C01b.class).build();
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(99, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C02a {}
+
+	@Test
+	public void c02a_minmax_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C02a.class).build();
+		s.validateOutput(11, BeanContext.DEFAULT);
+		s.validateOutput(99, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(
+		items=@Items(
+			minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100", maximum="1000", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000,maximum:10000,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000,maximum:100000,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C02b {}
+	
+	@Test
+	public void c02b_minmax_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C02b.class).build();
+		
+		s.getItems().validateOutput(11, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(99, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(99999, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(minimum="10.1", maximum="100.1")
+	public static class C03a {}
+	
+	@Test
+	public void c03_minmax_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C03a.class).build();
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(100.1f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(
+		items=@Items(
+			minimum="10.1", maximum="100.1",
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1",
+				items={
+					"minimum:1000.1,maximum:10000.1,",
+					"items:{minimum:10000.1,maximum:100000.1}"
+				}
+			)
+		)
+	)
+	public static class C03b {}
+	
+	@Test
+	public void c03b_minmax_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C03b.class).build();
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C04a {}
+
+	@Test
+	public void c04a_minmax_floats_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C04a.class).build();
+		s.validateOutput(10.2f, BeanContext.DEFAULT);
+		s.validateOutput(100f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(
+		items=@Items(
+			minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000.1,maximum:10000.1,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000.1,maximum:100000.1,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C04b {}
+	
+	@Test
+	public void c04b_minmax_floats_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C04b.class).build();
+
+		s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(multipleOf="10")
+	public static class C05a {}
+	
+	@Test
+	public void c05a_multipleOf() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C05a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(20, BeanContext.DEFAULT);
+		s.validateOutput(10f, BeanContext.DEFAULT);
+		s.validateOutput(20f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(
+		items=@Items(
+			multipleOf="10",
+			items=@SubItems(
+				multipleOf="100",
+				items={
+					"multipleOf:1000,",
+					"items:{multipleOf:10000}"
+				}
+			)
+		)
+	)
+	public static class C05b {}
+	
+	@Test
+	public void c05b_multipleOf_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C05b.class).build();
+
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(20f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Path(multipleOf="10.1")
+	public static class C06a {}
+	
+	@Test
+	public void c06a_multipleOf_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C06a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(20.2f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(
+		items=@Items(
+			multipleOf="10.1",
+			items=@SubItems(
+				multipleOf="100.1",
+				items={
+					"multipleOf:1000.1,",
+					"items:{multipleOf:10000.1}"
+				}
+			)
+		)
+	)
+	public static class C06b {}
+	
+	@Test
+	public void c06b_multipleOf_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, C06b.class).build();
+		
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000.2f, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Collections/Array validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Path(
+		items=@Items(
+			uniqueItems=true,
+			items=@SubItems(
+				uniqueItems=true,
+				items={
+					"uniqueItems:true,",
+					"items:{uniqueItems:true}"
+				}
+			)
+		)
+		
+	)
+	public static class D01 {}
+	
+	@Test
+	public void d01a_uniqueItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, D01.class).build();
+		
+		String[] good = split("a,b"), bad = split("a,a");
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Test
+	public void d01b_uniqueItems_collections() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, D01.class).build();
+		
+		AList<String> 
+			good = new AList<String>().appendAll(split("a,b")), 
+			bad = new AList<String>().appendAll(split("a,a")); 
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Path(
+		items=@Items(
+			minItems=1, maxItems=2,
+			items=@SubItems(
+				minItems=2, maxItems=3,
+				items={
+					"minItems:3,maxItems:4,",
+					"items:{minItems:4,maxItems:5}"
+				}
+			)
+		)
+		
+	)
+	public static class D02 {}
+	
+	@Test
+	public void d02a_minMaxItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Path.class, D02.class).build();
+		
+		s.getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(new String[0], BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+	}	
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Query.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Query.java
new file mode 100644
index 0000000..c8fb7a0
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Query.java
@@ -0,0 +1,1419 @@
+// ***************************************************************************************************************************
+// * 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.httppart;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+public class HttpPartSchemaTest_Query {
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Basic test
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testBasic() throws Exception {
+		HttpPartSchema.create().build();
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// @Query
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Query("x")
+	public static class A01 {}
+	
+	@Test
+	public void a01_value() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, A01.class).build();
+		assertEquals("x", s.getName());
+		assertObjectEquals("{}", s.getApi());
+	}
+	
+	@Query(
+		name="x",
+		type="number",
+		format="int32",
+		collectionFormat="csv",
+		maximum="1",
+		minimum="2",
+		multipleOf="3",
+		pattern="4",
+		maxLength=1,
+		minLength=2,
+		maxItems=3,
+		minItems=4,
+		exclusiveMaximum=true,
+		exclusiveMinimum=true,
+		uniqueItems=true,
+		required=true,
+		skipIfEmpty=true,
+		description={"b1","b2"},
+		_default={"c1","c2"},
+		items=@Items($ref="d1"),
+		_enum="e1,e2,e3",
+		example="f1",
+		api="{g1:true}"
+	)
+	public static class A02 {}
+
+	@Test
+	public void a02_basic_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, A02.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertEquals(3, s.getMaxItems().longValue());
+		assertEquals(4, s.getMinItems().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertTrue(s.getUniqueItems());
+		assertTrue(s.getRequired());
+		assertTrue(s.getSkipIfEmpty());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertEquals("c1\nc2", s.getDefault());
+		assertObjectEquals("{collectionFormat:'csv','default':'c1\\nc2',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxItems:3,maxLength:1,minimum:'2',minItems:4,minLength:2,multipleOf:'3',pattern:'4',required:true,type:'number',uniqueItems:true,_value:'{g1:true}'}", s.getApi());
+	}	
+
+	public static class A03 {
+		public void a(
+				@Query(
+					name="x",
+					type="number",
+					format="int32",
+					collectionFormat="csv",
+					maximum="1",
+					minimum="2",
+					multipleOf="3",
+					pattern="4",
+					maxLength=1,
+					minLength=2,
+					maxItems=3,
+					minItems=4,
+					exclusiveMaximum=true,
+					exclusiveMinimum=true,
+					uniqueItems=true,
+					required=true,
+					skipIfEmpty=true,
+					description={"b1","b2"},
+					_default={"c1","c2"},
+					items=@Items($ref="d1"),
+					_enum="e1,e2,e3",
+					example="f1",
+					api="{g1:true}"
+				) String x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a03_basic_onParameter() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, A03.class.getMethod("a", String.class), 0).noValidate(true).build();
+		assertEquals("x", s.getName());
+		assertEquals(HttpPartSchema.Type.NUMBER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT32, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, s.getCollectionFormat());
+		assertEquals(1, s.getMaximum());
+		assertEquals(2, s.getMinimum());
+		assertEquals(3, s.getMultipleOf());
+		assertEquals("4", s.getPattern().pattern());
+		assertEquals(1, s.getMaxLength().longValue());
+		assertEquals(2, s.getMinLength().longValue());
+		assertEquals(3, s.getMaxItems().longValue());
+		assertEquals(4, s.getMinItems().longValue());
+		assertTrue(s.getExclusiveMaximum());
+		assertTrue(s.getExclusiveMinimum());
+		assertTrue(s.getUniqueItems());
+		assertTrue(s.getRequired());
+		assertTrue(s.getSkipIfEmpty());
+		assertObjectEquals("['e1','e2','e3']", s.getEnum());
+		assertEquals("c1\nc2", s.getDefault());
+		assertObjectEquals("{collectionFormat:'csv','default':'c1\\nc2',description:'b1\\nb2','enum':['e1','e2','e3'],example:'f1',exclusiveMaximum:true,exclusiveMinimum:true,format:'int32',items:{'$ref':'d1'},maximum:'1',maxItems:3,maxLength:1,minimum:'2',minItems:4,minLength:2,multipleOf:'3',pattern:'4',required:true,type:'number',uniqueItems:true,_value:'{g1:true}'}", s.getApi());
+	}	
+	
+	public static class A04 {
+		public void a(
+				@Query(
+					name="y",
+					type="integer",
+					format="int64",
+					collectionFormat="ssv",
+					maximum="5",
+					minimum="6",
+					multipleOf="7",
+					pattern="8",
+					maxLength=5,
+					minLength=6,
+					maxItems=7,
+					minItems=8,
+					exclusiveMaximum=false,
+					exclusiveMinimum=false,
+					uniqueItems=false,
+					required=false,
+					skipIfEmpty=false,
+					description={"b3","b3"},
+					_default={"c3","c4"},
+					items=@Items($ref="d2"),
+					_enum="e4,e5,e6",
+					example="f2",
+					api="{g2:true}"
+				) A01 x
+			) {
+			
+		}
+	}
+
+	@Test
+	public void a04_basic_onParameterAndClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, A04.class.getMethod("a", A01.class), 0).noValidate(true).build();
+		assertEquals("y", s.getName());
+		assertEquals(HttpPartSchema.Type.INTEGER, s.getType());
+		assertEquals(HttpPartSchema.Format.INT64, s.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, s.getCollectionFormat());
+		assertEquals(5, s.getMaximum());
+		assertEquals(6, s.getMinimum());
+		assertEquals(7, s.getMultipleOf());
+		assertEquals("8", s.getPattern().pattern());
+		assertEquals(5, s.getMaxLength().longValue());
+		assertEquals(6, s.getMinLength().longValue());
+		assertEquals(7, s.getMaxItems().longValue());
+		assertEquals(8, s.getMinItems().longValue());
+		assertNull(s.getExclusiveMaximum());
+		assertNull(s.getExclusiveMinimum());
+		assertNull(s.getUniqueItems());
+		assertNull(s.getRequired());
+		assertNull(s.getSkipIfEmpty());
+		assertObjectEquals("['e4','e5','e6']", s.getEnum());
+		assertEquals("c3\nc4", s.getDefault());
+		assertObjectEquals("{collectionFormat:'ssv','default':'c3\\nc4',description:'b3\\nb3','enum':['e4','e5','e6'],example:'f2',format:'int64',items:{'$ref':'d2'},maximum:'5',maxItems:7,maxLength:5,minimum:'6',minItems:8,minLength:6,multipleOf:'7',pattern:'8',type:'integer',_value:'{g2:true}'}", s.getApi());
+	}	
+
+	@Query(
+		name="x",
+		items=@Items(
+			type="number",
+			format="int32",
+			collectionFormat="csv",
+			maximum="1",
+			minimum="2",
+			multipleOf="3",
+			pattern="4",
+			maxLength=1,
+			minLength=2,
+			maxItems=3,
+			minItems=4,
+			exclusiveMaximum=true,
+			exclusiveMinimum=true,
+			uniqueItems=true,
+			_default={"c1","c2"},
+			_enum="e1,e2",
+			items=@SubItems(
+				type="integer",
+				format="int64",
+				collectionFormat="ssv",
+				maximum="5",
+				minimum="6",
+				multipleOf="7",
+				pattern="8",
+				maxLength=5,
+				minLength=6,
+				maxItems=7,
+				minItems=8,
+				exclusiveMaximum=false,
+				exclusiveMinimum=false,
+				uniqueItems=false,
+				_default={"c3","c4"},
+				_enum="e3,e4",
+				items={
+					"type:'string',",
+					"format:'float',",
+					"collectionFormat:'tsv',",
+					"maximum:'9',",
+					"minimum:'10',",
+					"multipleOf:'11',",
+					"pattern:'12',",
+					"maxLength:9,",
+					"minLength:10,",
+					"maxItems:11,",
+					"minItems:12,",
+					"exclusiveMaximum:true,",
+					"exclusiveMinimum:true,",
+					"uniqueItems:true,",
+					"default:'c5\\nc6',",
+					"enum:['e5','e6'],",
+					"items:{",
+						"type:'array',",
+						"format:'double',",
+						"collectionFormat:'pipes',",
+						"maximum:'13',",
+						"minimum:'14',",
+						"multipleOf:'15',",
+						"pattern:'16',",
+						"maxLength:13,",
+						"minLength:14,",
+						"maxItems:15,",
+						"minItems:16,",
+						"exclusiveMaximum:false,",
+						"exclusiveMinimum:false,",
+						"uniqueItems:false,",
+						"default:'c7\\nc8',",
+						"enum:['e7','e8']",
+					"}"
+				}
+			)
+		)
+	)
+	public static class A05 {}
+	
+	@Test
+	public void a05_basic_nestedItems_onClass() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, A05.class).noValidate(true).build();
+		assertEquals("x", s.getName());
+		
+		HttpPartSchema items = s.getItems();
+		assertEquals(HttpPartSchema.Type.NUMBER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT32, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.CSV, items.getCollectionFormat());
+		assertEquals(1, items.getMaximum());
+		assertEquals(2, items.getMinimum());
+		assertEquals(3, items.getMultipleOf());
+		assertEquals("4", items.getPattern().pattern());
+		assertEquals(1, items.getMaxLength().longValue());
+		assertEquals(2, items.getMinLength().longValue());
+		assertEquals(3, items.getMaxItems().longValue());
+		assertEquals(4, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e1','e2']", items.getEnum());
+		assertEquals("c1\nc2", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.INTEGER, items.getType());
+		assertEquals(HttpPartSchema.Format.INT64, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.SSV, items.getCollectionFormat());
+		assertEquals(5, items.getMaximum());
+		assertEquals(6, items.getMinimum());
+		assertEquals(7, items.getMultipleOf());
+		assertEquals("8", items.getPattern().pattern());
+		assertEquals(5, items.getMaxLength().longValue());
+		assertEquals(6, items.getMinLength().longValue());
+		assertEquals(7, items.getMaxItems().longValue());
+		assertEquals(8, items.getMinItems().longValue());
+		assertNull(items.getExclusiveMaximum());
+		assertNull(items.getExclusiveMinimum());
+		assertNull(items.getUniqueItems());
+		assertObjectEquals("['e3','e4']", items.getEnum());
+		assertEquals("c3\nc4", items.getDefault());
+
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.STRING, items.getType());
+		assertEquals(HttpPartSchema.Format.FLOAT, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.TSV, items.getCollectionFormat());
+		assertEquals(9, items.getMaximum());
+		assertEquals(10, items.getMinimum());
+		assertEquals(11, items.getMultipleOf());
+		assertEquals("12", items.getPattern().pattern());
+		assertEquals(9, items.getMaxLength().longValue());
+		assertEquals(10, items.getMinLength().longValue());
+		assertEquals(11, items.getMaxItems().longValue());
+		assertEquals(12, items.getMinItems().longValue());
+		assertTrue(items.getExclusiveMaximum());
+		assertTrue(items.getExclusiveMinimum());
+		assertTrue(items.getUniqueItems());
+		assertObjectEquals("['e5','e6']", items.getEnum());
+		assertEquals("c5\nc6", items.getDefault());
+		
+		items = items.getItems();
+		assertEquals(HttpPartSchema.Type.ARRAY, items.getType());
+		assertEquals(HttpPartSchema.Format.DOUBLE, items.getFormat());
+		assertEquals(HttpPartSchema.CollectionFormat.PIPES, items.getCollectionFormat());
+		assertEquals(13, items.getMaximum());
+		assertEquals(14, items.getMinimum());
+		assertEquals(15, items.getMultipleOf());
+		assertEquals("16", items.getPattern().pattern());
+		assertEquals(13, items.getMaxLength().longValue());
+		assertEquals(14, items.getMinLength().longValue());
+		assertEquals(15, items.getMaxItems().longValue());
+		assertEquals(16, items.getMinItems().longValue());
+		assertFalse(items.getExclusiveMaximum());
+		assertFalse(items.getExclusiveMinimum());
+		assertFalse(items.getUniqueItems());
+		assertObjectEquals("['e7','e8']", items.getEnum());
+		assertEquals("c7\nc8", items.getDefault());
+
+		assertObjectEquals(
+			"{items:{collectionFormat:'csv','default':'c1\\nc2','enum':['e1','e2'],format:'int32',exclusiveMaximum:true,exclusiveMinimum:true,items:{collectionFormat:'ssv','default':'c3\\nc4','enum':['e3','e4'],format:'int64',items:{type:'string',format:'float',collectionFormat:'tsv',maximum:'9',minimum:'10',multipleOf:'11',pattern:'12',maxLength:9,minLength:10,maxItems:11,minItems:12,exclusiveMaximum:true,exclusiveMinimum:true,uniqueItems:true,'default':'c5\\nc6','enum':['e5','e6'],items:{type:' [...]
+			s.getApi()
+		);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// String input validations.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Query(required=true)
+	public static class B01a {}
+	
+	@Test
+	public void b01a_required() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B01a.class).build();
+		
+		s.validateInput("x");
+		
+		try {
+			s.validateInput(null);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("No value specified.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(allowEmptyValue=true)
+	public static class B01b {}
+	
+	@Test
+	public void b01b_allowEmptyValue() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B01b.class).build();
+		
+		s.validateInput("");  
+		s.validateInput(null);
+	}
+
+	@Query(required=true,allowEmptyValue=true)
+	public static class B01c {}
+	
+	@Test
+	public void b01b_required_allowEmptyValue() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B01c.class).build();
+		
+		s.validateInput("");  
+
+		try {
+			s.validateInput(null);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("No value specified.", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(pattern="x.*")
+	public static class B02a {}
+	
+	@Test
+	public void b02a_pattern() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B02a.class).build();
+		s.validateInput("x");
+		s.validateInput("xx");
+		try {
+			s.validateInput("y");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("yx");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match expected pattern.  Must match pattern: x.*", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("");  // Empty headers are never allowed.
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Empty value not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(minLength=2, maxLength=3)
+	public static class B03a {}
+
+	@Test
+	public void b03a_length() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B03a.class).build();
+		s.validateInput("12");
+		s.validateInput("123");
+		s.validateInput(null);
+		try {
+			s.validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(
+		items=@Items(
+			minLength=2, maxLength=3,
+			items=@SubItems(
+				minLength=3, maxLength=4,
+				items={
+					"minLength:4,maxLength:5,",
+					"items:{minLength:5,maxLength:6}"
+				}
+			)
+		)
+	)
+	public static class B03b {}
+
+	@Test
+	public void b03b_length_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B03b.class).build();
+		
+		s.getItems().validateInput("12");
+		s.getItems().getItems().validateInput("123");
+		s.getItems().getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().getItems().validateInput("12345");
+		
+		s.getItems().validateInput("123");
+		s.getItems().getItems().validateInput("1234");
+		s.getItems().getItems().getItems().validateInput("12345");
+		s.getItems().getItems().getItems().getItems().validateInput("123456");
+		
+		s.getItems().validateInput(null);
+		s.getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().validateInput(null);
+		s.getItems().getItems().getItems().getItems().validateInput(null);
+		
+		try {
+			s.getItems().validateInput("1");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum length of value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateInput("1234");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("12345");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("123456");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("1234567");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum length of value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(_enum="X,Y")
+	public static class B04a {}
+	
+	@Test
+	public void b04a_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B04a.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(_enum=" X , Y ")
+	public static class B04b {}
+	
+	@Test
+	public void b04b_enum() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B04b.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(_enum="['X','Y']")
+	public static class B04c {}
+	
+	@Test
+	public void b04c_enum_json() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B04c.class).build();
+		s.validateInput("X");
+		s.validateInput("Y");
+		s.validateInput(null);
+		try {
+			s.validateInput("Z");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X','Y']", e.getLocalizedMessage());
+		}
+	}	
+	
+	@Query(
+		items=@Items(
+			_enum="['W']",
+			items=@SubItems(
+				_enum="['X']",
+				items={
+					"enum:['Y'],",
+					"items:{enum:['Z']}"
+				}
+			)
+		)
+	)
+	public static class B04d {}
+	
+	@Test
+	public void b04d_enum_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, B04d.class).build();
+
+		s.getItems().validateInput("W");
+		s.getItems().getItems().validateInput("X");
+		s.getItems().getItems().getItems().validateInput("Y");
+		s.getItems().getItems().getItems().getItems().validateInput("Z");
+		
+		try {
+			s.getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['W']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['X']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Y']", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateInput("V");
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Value does not match one of the expected values.  Must be one of the following: ['Z']", e.getLocalizedMessage());
+		}
+	}	
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Numeric validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Query(minimum="10", maximum="100")
+	public static class C01a {}
+	
+	@Test
+	public void c01a_minmax_ints() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C01a.class).build();
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(100, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(
+		items=@Items(
+			minimum="10", maximum="100",
+			items=@SubItems(
+				minimum="100", maximum="1000",
+				items={
+					"minimum:1000,maximum:10000,",
+					"items:{minimum:10000,maximum:100000}"
+				}
+			)
+		)
+	)
+	public static class C01b {}
+	
+	@Test
+	public void c01b_minmax_ints_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C01b.class).build();
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(9, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(99, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C02a {}
+
+	@Test
+	public void c02a_minmax_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C02a.class).build();
+		s.validateOutput(11, BeanContext.DEFAULT);
+		s.validateOutput(99, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(
+		items=@Items(
+			minimum="10", maximum="100", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100", maximum="1000", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000,maximum:10000,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000,maximum:100000,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C02b {}
+	
+	@Test
+	public void c02b_minmax_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C02b.class).build();
+		
+		s.getItems().validateOutput(11, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(99, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(9999, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(99999, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(minimum="10.1", maximum="100.1")
+	public static class C03a {}
+	
+	@Test
+	public void c03_minmax_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C03a.class).build();
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(100.1f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(
+		items=@Items(
+			minimum="10.1", maximum="100.1",
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1",
+				items={
+					"minimum:1000.1,maximum:10000.1,",
+					"items:{minimum:10000.1,maximum:100000.1}"
+				}
+			)
+		)
+	)
+	public static class C03b {}
+	
+	@Test
+	public void c03b_minmax_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C03b.class).build();
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true)
+	public static class C04a {}
+
+	@Test
+	public void c04a_minmax_floats_exclusive() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C04a.class).build();
+		s.validateOutput(10.2f, BeanContext.DEFAULT);
+		s.validateOutput(100f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(
+		items=@Items(
+			minimum="10.1", maximum="100.1", exclusiveMinimum=true, exclusiveMaximum=true,
+			items=@SubItems(
+				minimum="100.1", maximum="1000.1", exclusiveMinimum=true, exclusiveMaximum=true,
+				items={
+					"minimum:1000.1,maximum:10000.1,exclusiveMinimum:true,exclusiveMaximum:true,",
+					"items:{minimum:10000.1,maximum:100000.1,exclusiveMinimum:true,exclusiveMaximum:true}"
+				}
+			)
+		)
+	)
+	public static class C04b {}
+	
+	@Test
+	public void c04b_minmax_floats_exclusive_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C04b.class).build();
+
+		s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(100000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum value not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(100000.1f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum value exceeded.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(multipleOf="10")
+	public static class C05a {}
+	
+	@Test
+	public void c05a_multipleOf() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C05a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10, BeanContext.DEFAULT);
+		s.validateOutput(20, BeanContext.DEFAULT);
+		s.validateOutput(10f, BeanContext.DEFAULT);
+		s.validateOutput(20f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(
+		items=@Items(
+			multipleOf="10",
+			items=@SubItems(
+				multipleOf="100",
+				items={
+					"multipleOf:1000,",
+					"items:{multipleOf:10000}"
+				}
+			)
+		)
+	)
+	public static class C05b {}
+	
+	@Test
+	public void c05b_multipleOf_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C05b.class).build();
+
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+	
+		s.getItems().validateOutput(10, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000f, BeanContext.DEFAULT);
+
+		s.getItems().validateOutput(20f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000f, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(11, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(101, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10001, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	@Query(multipleOf="10.1")
+	public static class C06a {}
+	
+	@Test
+	public void c06a_multipleOf_floats() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C06a.class).build();
+		s.validateOutput(0, BeanContext.DEFAULT);
+		s.validateOutput(10.1f, BeanContext.DEFAULT);
+		s.validateOutput(20.2f, BeanContext.DEFAULT);
+		s.validateOutput(null, BeanContext.DEFAULT);
+		try {
+			s.validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(
+		items=@Items(
+			multipleOf="10.1",
+			items=@SubItems(
+				multipleOf="100.1",
+				items={
+					"multipleOf:1000.1,",
+					"items:{multipleOf:10000.1}"
+				}
+			)
+		)
+	)
+	public static class C06b {}
+	
+	@Test
+	public void c06b_multipleOf_floats_items() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, C06b.class).build();
+		
+		s.getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(0, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(10.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(100.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(1000.1f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(10000.1f, BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(20.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(200.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(2000.2f, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(20000.2f, BeanContext.DEFAULT);
+		
+		try {
+			s.getItems().validateOutput(10.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(100.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(1000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(10000.2f, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Multiple-of not met.", e.getLocalizedMessage());
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Collections/Array validations
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Query(
+		items=@Items(
+			uniqueItems=true,
+			items=@SubItems(
+				uniqueItems=true,
+				items={
+					"uniqueItems:true,",
+					"items:{uniqueItems:true}"
+				}
+			)
+		)
+		
+	)
+	public static class D01 {}
+	
+	@Test
+	public void d01a_uniqueItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, D01.class).build();
+		
+		String[] good = split("a,b"), bad = split("a,a");
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Test
+	public void d01b_uniqueItems_collections() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, D01.class).build();
+		
+		AList<String> 
+			good = new AList<String>().appendAll(split("a,b")), 
+			bad = new AList<String>().appendAll(split("a,a")); 
+		
+		s.getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(good, BeanContext.DEFAULT);
+		s.getItems().validateOutput(null, BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(bad, BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Duplicate items not allowed.", e.getLocalizedMessage());
+		}
+	}
+	
+	@Query(
+		items=@Items(
+			minItems=1, maxItems=2,
+			items=@SubItems(
+				minItems=2, maxItems=3,
+				items={
+					"minItems:3,maxItems:4,",
+					"items:{minItems:4,maxItems:5}"
+				}
+			)
+		)
+		
+	)
+	public static class D02 {}
+	
+	@Test
+	public void d02a_minMaxItems_arrays() throws Exception {
+		HttpPartSchema s = HttpPartSchema.create().apply(Query.class, D02.class).build();
+		
+		s.getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		
+		s.getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+		s.getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+		s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+
+		try {
+			s.getItems().validateOutput(new String[0], BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Minimum number of items not met.", e.getLocalizedMessage());
+		}
+		
+		try {
+			s.getItems().validateOutput(split("1,2,3"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().validateOutput(split("1,2,3,4"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().validateOutput(split("1,2,3,4,5"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+		try {
+			s.getItems().getItems().getItems().getItems().validateOutput(split("1,2,3,4,5,6"), BeanContext.DEFAULT);
+			fail();
+		} catch (SchemaValidationParseException e) {
+			assertEquals("Maximum number of items exceeded.", e.getLocalizedMessage());
+		}
+	}	
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/testutils/TestUtils.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/testutils/TestUtils.java
index abdfced..5aff11f 100644
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/testutils/TestUtils.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/testutils/TestUtils.java
@@ -382,6 +382,8 @@ public class TestUtils {
 	 * Assert that the object equals the specified string after running it through ws.toString().
 	 */
 	public static final void assertObjectEquals(String s, Object o, WriterSerializer ws) {
+		if ("xxx".equals(s)) 
+			System.err.println(ws.toString(o).replaceAll("\\\\", "\\\\\\\\"));
 		Assert.assertEquals(s, ws.toString(o));
 	}
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/AnnotationUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/AnnotationUtils.java
similarity index 76%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/AnnotationUtils.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/AnnotationUtils.java
index 625a258..c09d782 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/AnnotationUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/AnnotationUtils.java
@@ -10,13 +10,16 @@
 // * "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.rest.util;
+package org.apache.juneau.http.annotation;
 
 import static org.apache.juneau.internal.StringUtils.*;
 
+import java.util.*;
+
 import org.apache.juneau.*;
-import org.apache.juneau.rest.annotation.*;
-import org.apache.juneau.rest.exception.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Various reusable utility methods when working with annotations.
@@ -45,12 +48,14 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.value()))
 			.appendSkipEmpty("description", joinnl(a.description()))
-			.appendSkipFalse("required", a.required())
 			.appendSkipEmpty("example", joinnl(a.example()))
 			.appendSkipEmpty("examples", joinnl(a.examples()))
-			.appendSkipEmpty("schema", merge(om.getObjectMap("schema"), a.schema()));
+			.appendSkipFalse("required", a.required())
+			.appendSkipEmpty("schema", merge(om.getObjectMap("schema"), a.schema()))
+			.appendSkipEmpty("_value", joinnl(a.value()))
+			.appendSkipEmpty("_value", joinnl(a.api()))
+		;
 	}
 
 	/**
@@ -65,9 +70,10 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.value()))
 			.appendSkipEmpty("description", joinnl(a.description()))
-			.appendSkipEmpty("url", a.url());
+			.appendSkipEmpty("url", a.url())
+			.appendSkipEmpty("_value", joinnl(a.value()))
+		;
 	}
 
 	/**
@@ -81,41 +87,41 @@ public class AnnotationUtils {
 		if (empty(a))
 			return om;
 		om = newMap(om);
-		a._enum();
 		return om
-			.appendSkipEmpty("_value", joinnl(a.value()))
-			.appendSkipEmpty("$ref", a.$ref())
-			.appendSkipEmpty("format", a.format())
-			.appendSkipEmpty("title", a.title())
-			.appendSkipEmpty("description", joinnl(a.description()))
+			.appendSkipEmpty("additionalProperties", toObjectMap(a.additionalProperties()))
+			.appendSkipEmpty("allOf", joinnl(a.allOf()))
 			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("multipleOf", a.multipleOf())
-			.appendSkipEmpty("maximum", a.maximum())
+			.appendSkipEmpty("discriminator", a.discriminator())
+			.appendSkipEmpty("description", joinnl(a.description()))
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipEmpty("example", joinnl(a.example()))
+			.appendSkipEmpty("examples", joinnl(a.examples()))
 			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipEmpty("minimum", a.minimum())
 			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipEmpty("externalDocs", merge(om.getObjectMap("externalDocs"), a.externalDocs()))
+			.appendSkipEmpty("format", a.format())
+			.appendSkipEmpty("ignore", a.ignore() ? "true" : null)
+			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
+			.appendSkipEmpty("maximum", a.maximum())
+			.appendSkipMinusOne("maxItems", a.maxItems())
 			.appendSkipMinusOne("maxLength", a.maxLength())
+			.appendSkipMinusOne("maxProperties", a.maxProperties())
+			.appendSkipEmpty("minimum", a.minimum())
+			.appendSkipMinusOne("minItems", a.minItems())
 			.appendSkipMinusOne("minLength", a.minLength())
+			.appendSkipMinusOne("minProperties", a.minProperties())
+			.appendSkipEmpty("multipleOf", a.multipleOf())
 			.appendSkipEmpty("pattern", a.pattern())
-			.appendSkipMinusOne("maxItems", a.maxItems())
-			.appendSkipMinusOne("minItems", a.minItems())
-			.appendSkipFalse("uniqueItems", a.uniqueItems())
-			.appendSkipEmpty("maxProperties", a.maxProperties())
-			.appendSkipEmpty("minProperties", a.minProperties())
+			.appendSkipEmpty("properties", toObjectMap(a.properties()))
+			.appendSkipFalse("readOnly", a.readOnly())
 			.appendSkipFalse("required", a.required())
-			.appendSkipEmpty("enum", joinnl(a._enum()))
+			.appendSkipEmpty("title", a.title())
 			.appendSkipEmpty("type", a.type())
-			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
-			.appendSkipEmpty("allOf", joinnl(a.allOf()))
-			.appendSkipEmpty("properties", joinnl(a.properties()))
-			.appendSkipEmpty("additionalProperties", joinnl(a.additionalProperties()))
-			.appendSkipEmpty("discriminator", a.discriminator())
-			.appendSkipFalse("readOnly", a.readOnly())
+			.appendSkipFalse("uniqueItems", a.uniqueItems())
 			.appendSkipEmpty("xml", joinnl(a.xml()))
-			.appendSkipEmpty("externalDocs", merge(om.getObjectMap("externalDocs"), a.externalDocs()))
-			.appendSkipEmpty("example", joinnl(a.example()))
-			.appendSkipEmpty("examples", joinnl(a.examples()))
-			.appendSkipEmpty("ignore", a.ignore() ? "true" : null);
+			.appendSkipEmpty("_value", joinnl(a.value()))
+			.appendSkipEmpty("$ref", a.$ref())
+		;
 	}
 
 	/**
@@ -130,12 +136,13 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.api()))
 			.appendSkipEmpty("description", joinnl(a.description()))
 			.appendSkipEmpty("example", joinnl(a.example()))
 			.appendSkipEmpty("examples", joinnl(a.examples()))
+			.appendSkipEmpty("headers", merge(om.getObjectMap("headers"), a.headers()))
 			.appendSkipEmpty("schema", merge(om.getObjectMap("schema"), a.schema()))
-			.appendSkipEmpty("headers", merge(om.getObjectMap("headers"), a.headers()));
+			.appendSkipEmpty("_value", joinnl(a.api()))
+		;
 	}	
 	
 	/**
@@ -152,7 +159,7 @@ public class AnnotationUtils {
 		for (ResponseHeader aa : a) {
 			String name = firstNonEmpty(aa.name(), aa.value());
 			if (isEmpty(name))
-				throw new InternalServerError("@ResponseHeader used without name or value.");
+				throw new RuntimeException("@ResponseHeader used without name or value.");
 			om.getObjectMap(name, true).putAll(merge(null, aa));
 		}
 		return om;
@@ -170,26 +177,26 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.value()))
-			.appendSkipEmpty("type", a.type())
-			.appendSkipEmpty("format", a.format())
 			.appendSkipEmpty("collectionFormat", a.collectionFormat())
-			.appendSkipEmpty("pattern", a.pattern())
-			.appendSkipEmpty("$ref", a.$ref())
+			.appendSkipEmpty("default", joinnl(a._default()))
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipEmpty("format", a.format())
+			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
+			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
 			.appendSkipEmpty("maximum", a.maximum())
-			.appendSkipEmpty("minimum", a.minimum())
-			.appendSkipEmpty("multipleOf", a.multipleOf())
-			.appendSkipMinusOne("maxLength", a.maxLength())
-			.appendSkipMinusOne("minLength", a.minLength())
 			.appendSkipMinusOne("maxItems", a.maxItems())
+			.appendSkipMinusOne("maxLength", a.maxLength())
+			.appendSkipEmpty("minimum", a.minimum())
 			.appendSkipMinusOne("minItems", a.minItems())
-			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipMinusOne("minLength", a.minLength())
+			.appendSkipEmpty("multipleOf", a.multipleOf())
+			.appendSkipEmpty("pattern", a.pattern())
 			.appendSkipFalse("uniqueItems", a.uniqueItems())
-			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("enum", joinnl(a._enum()))
-			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
-			;
+			.appendSkipEmpty("type", a.type())
+			.appendSkipEmpty("$ref", a.$ref())
+			.appendSkipEmpty("_value", joinnl(a.value()))
+		;
 	}	
 
 	/**
@@ -204,26 +211,26 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.value()))
-			.appendSkipEmpty("type", a.type())
-			.appendSkipEmpty("format", a.format())
 			.appendSkipEmpty("collectionFormat", a.collectionFormat())
-			.appendSkipEmpty("pattern", a.pattern())
-			.appendSkipEmpty("$ref", a.$ref())
+			.appendSkipEmpty("default", joinnl(a._default()))
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
+			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipEmpty("format", a.format())
+			.appendSkipEmpty("items", toObjectMap(a.items()))
 			.appendSkipEmpty("maximum", a.maximum())
-			.appendSkipEmpty("minimum", a.minimum())
-			.appendSkipEmpty("multipleOf", a.multipleOf())
-			.appendSkipMinusOne("maxLength", a.maxLength())
-			.appendSkipMinusOne("minLength", a.minLength())
 			.appendSkipMinusOne("maxItems", a.maxItems())
+			.appendSkipMinusOne("maxLength", a.maxLength())
+			.appendSkipEmpty("minimum", a.minimum())
 			.appendSkipMinusOne("minItems", a.minItems())
-			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipMinusOne("minLength", a.minLength())
+			.appendSkipEmpty("multipleOf", a.multipleOf())
+			.appendSkipEmpty("pattern", a.pattern())
+			.appendSkipEmpty("type", a.type())
 			.appendSkipFalse("uniqueItems", a.uniqueItems())
-			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("enum", joinnl(a._enum()))
-			.appendSkipEmpty("items", joinnl(a.items()))
-			;
+			.appendSkipEmpty("$ref", a.$ref())
+			.appendSkipEmpty("_value", joinnl(a.value()))
+		;
 	}	
 
 	/**
@@ -238,27 +245,27 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om 
-			.appendSkipEmpty("_value", joinnl(a.api()))
-			.appendSkipEmpty("$ref", a.$ref())
+			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("default", joinnl(a._default()))
 			.appendSkipEmpty("description", joinnl(a.description()))
-			.appendSkipEmpty("type", a.type())
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipEmpty("example", joinnl(a.example()))
+			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
+			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
 			.appendSkipEmpty("format", a.format())
-			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
 			.appendSkipEmpty("maximum", a.maximum())
-			.appendSkipEmpty("minimum", a.minimum())
-			.appendSkipEmpty("multipleOf", a.multipleOf())
 			.appendSkipMinusOne("maxLength", a.maxLength())
-			.appendSkipMinusOne("minLength", a.minLength())
 			.appendSkipMinusOne("maxItems", a.maxItems())
+			.appendSkipEmpty("minimum", a.minimum())
+			.appendSkipMinusOne("minLength", a.minLength())
 			.appendSkipMinusOne("minItems", a.minItems())
-			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipEmpty("multipleOf", a.multipleOf())
 			.appendSkipFalse("uniqueItems", a.uniqueItems())
-			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
-			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("enum", joinnl(a._enum()))
-			.appendSkipEmpty("example", joinnl(a.example()));
+			.appendSkipEmpty("type", a.type())
+			.appendSkipEmpty("$ref", a.$ref())
+			.appendSkipEmpty("_value", joinnl(a.api()))
+		;
 	}
 	
 	/**
@@ -289,22 +296,23 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.api()))
+			.appendSkipEmpty("collectionFormat", a.collectionFormat())
 			.appendSkipEmpty("description", joinnl(a.description()))
-			.appendSkipEmpty("type", a.type())
-			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipEmpty("example", joinnl(a.example()))
+			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
+			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
 			.appendSkipEmpty("format", a.format())
-			.appendSkipEmpty("pattern", a.pattern())
-			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
 			.appendSkipEmpty("maximum", a.maximum())
-			.appendSkipEmpty("minimum", a.minimum())
-			.appendSkipEmpty("multipleOf", a.multipleOf())
 			.appendSkipMinusOne("maxLength", a.maxLength())
+			.appendSkipEmpty("minimum", a.minimum())
 			.appendSkipMinusOne("minLength", a.minLength())
-			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
-			.appendSkipEmpty("enum", joinnl(a._enum()))
-			.appendSkipEmpty("example", joinnl(a.example()));
+			.appendSkipEmpty("multipleOf", a.multipleOf())
+			.appendSkipEmpty("pattern", a.pattern())
+			.appendSkipEmpty("type", a.type())
+			.appendSkipEmpty("_value", joinnl(a.api()))
+		;
 	}
 	
 	/**
@@ -319,30 +327,31 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.api()))
+			.appendSkipFalse("allowEmptyValue", a.allowEmptyValue())
+			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("default", joinnl(a._default()))
 			.appendSkipEmpty("description", joinnl(a.description()))
-			.appendSkipFalse("required", a.required())
-			.appendSkipEmpty("type", a.type())
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipEmpty("example", joinnl(a.example()))
+			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
+			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
 			.appendSkipEmpty("format", a.format())
-			.appendSkipEmpty("pattern", a.pattern())
-			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
 			.appendSkipEmpty("maximum", a.maximum())
-			.appendSkipEmpty("minimum", a.minimum())
-			.appendSkipEmpty("multipleOf", a.multipleOf())
-			.appendSkipMinusOne("maxLength", a.maxLength())
-			.appendSkipMinusOne("minLength", a.minLength())
 			.appendSkipMinusOne("maxItems", a.maxItems())
+			.appendSkipMinusOne("maxLength", a.maxLength())
+			.appendSkipEmpty("minimum", a.minimum())
 			.appendSkipMinusOne("minItems", a.minItems())
-			.appendSkipFalse("allowEmptyValue", a.allowEmptyValue())
-			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipMinusOne("minLength", a.minLength())
+			.appendSkipEmpty("multipleOf", a.multipleOf())
+			.appendSkipEmpty("pattern", a.pattern())
+			.appendSkipFalse("required", a.required())
+			.appendSkipEmpty("type", a.type())
 			.appendSkipFalse("uniqueItems", a.uniqueItems())
-			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("enum", joinnl(a._enum()))
-			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
-			.appendSkipEmpty("example", joinnl(a.example()));
+			.appendSkipEmpty("_value", joinnl(a.api()))
+		;
 	}
-	
+
 	/**
 	 * Merges the contents of the specified annotation into the specified map.
 	 * 
@@ -355,27 +364,28 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.api()))
+			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("default", joinnl(a._default()))
 			.appendSkipEmpty("description", joinnl(a.description()))
-			.appendSkipFalse("required", a.required())
-			.appendSkipEmpty("type", a.type())
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipEmpty("example", joinnl(a.example()))
+			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
+			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
 			.appendSkipEmpty("format", a.format())
-			.appendSkipEmpty("pattern", a.pattern())
-			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
 			.appendSkipEmpty("maximum", a.maximum())
-			.appendSkipEmpty("minimum", a.minimum())
-			.appendSkipEmpty("multipleOf", a.multipleOf())
-			.appendSkipMinusOne("maxLength", a.maxLength())
-			.appendSkipMinusOne("minLength", a.minLength())
 			.appendSkipMinusOne("maxItems", a.maxItems())
+			.appendSkipMinusOne("maxLength", a.maxLength())
+			.appendSkipEmpty("minimum", a.minimum())
 			.appendSkipMinusOne("minItems", a.minItems())
-			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipMinusOne("minLength", a.minLength())
+			.appendSkipEmpty("multipleOf", a.multipleOf())
+			.appendSkipEmpty("pattern", a.pattern())
+			.appendSkipFalse("required", a.required())
+			.appendSkipEmpty("type", a.type())
 			.appendSkipFalse("uniqueItems", a.uniqueItems())
-			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("enum", joinnl(a._enum()))
-			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
-			.appendSkipEmpty("example", joinnl(a.example()));
+			.appendSkipEmpty("_value", joinnl(a.api()))
+		;
 	}
 
 	/**
@@ -390,28 +400,29 @@ public class AnnotationUtils {
 			return om;
 		om = newMap(om);
 		return om
-			.appendSkipEmpty("_value", joinnl(a.api()))
+			.appendSkipFalse("allowEmptyValue", a.allowEmptyValue())
+			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("default", joinnl(a._default()))
 			.appendSkipEmpty("description", joinnl(a.description()))
-			.appendSkipFalse("required", a.required())
-			.appendSkipEmpty("type", a.type())
+			.appendSkipEmpty("enum", toSet(a._enum()))
+			.appendSkipEmpty("example", joinnl(a.example()))
+			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
+			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
 			.appendSkipEmpty("format", a.format())
-			.appendSkipEmpty("pattern", a.pattern())
-			.appendSkipEmpty("collectionFormat", a.collectionFormat())
+			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
 			.appendSkipEmpty("maximum", a.maximum())
-			.appendSkipEmpty("minimum", a.minimum())
-			.appendSkipEmpty("multipleOf", a.multipleOf())
-			.appendSkipMinusOne("maxLength", a.maxLength())
-			.appendSkipMinusOne("minLength", a.minLength())
 			.appendSkipMinusOne("maxItems", a.maxItems())
+			.appendSkipMinusOne("maxLength", a.maxLength())
+			.appendSkipEmpty("minimum", a.minimum())
 			.appendSkipMinusOne("minItems", a.minItems())
-			.appendSkipFalse("allowEmptyValue", a.allowEmptyValue())
-			.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
-			.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
+			.appendSkipMinusOne("minLength", a.minLength())
+			.appendSkipEmpty("multipleOf", a.multipleOf())
+			.appendSkipEmpty("pattern", a.pattern())
+			.appendSkipFalse("required", a.required())
+			.appendSkipEmpty("type", a.type())
 			.appendSkipFalse("uniqueItems", a.uniqueItems())
-			.appendSkipEmpty("default", joinnl(a._default()))
-			.appendSkipEmpty("enum", joinnl(a._enum()))
-			.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
-			.appendSkipEmpty("example", joinnl(a.example()));
+			.appendSkipEmpty("_value", joinnl(a.api()))
+		;
 	}
 	
 	//=================================================================================================================
@@ -428,10 +439,10 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.description(), a._default(), a.example(), a.api())
-			&& empty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.collectionFormat(), a.maximum(), a.minimum(), a.multipleOf())
-			&& empty(a.allowEmptyValue(), a.exclusiveMaximum(), a.exclusiveMinimum(), a.required(), a.uniqueItems())
-			&& empty(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
+			allEmpty(a.description(), a._default(), a.example(), a.api())
+			&& allEmpty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.collectionFormat(), a.maximum(), a.minimum(), a.multipleOf())
+			&& allFalse(a.allowEmptyValue(), a.exclusiveMaximum(), a.exclusiveMinimum(), a.required(), a.uniqueItems())
+			&& allMinusOne(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
 			&& empty(a.items());
 	}
 
@@ -445,10 +456,10 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.description(), a._default(), a._enum(), a.example(), a.api())
-			&& empty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.collectionFormat(), a.maximum(), a.minimum(), a.multipleOf())
-			&& empty(a.exclusiveMaximum(), a.exclusiveMinimum(), a.required(), a.uniqueItems())
-			&& empty(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
+			allEmpty(a.description(), a._default(), a._enum(), a.example(), a.api())
+			&& allEmpty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.collectionFormat(), a.maximum(), a.minimum(), a.multipleOf())
+			&& allFalse(a.exclusiveMaximum(), a.exclusiveMinimum(), a.required(), a.uniqueItems())
+			&& allMinusOne(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
 			&& empty(a.items());
 	}
 
@@ -462,10 +473,10 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.description(), a._default(), a._enum(), a.example(), a.api())
-			&& empty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.collectionFormat(), a.maximum(), a.minimum(), a.multipleOf())
-			&& empty(a.allowEmptyValue(), a.exclusiveMaximum(), a.exclusiveMinimum(), a.required(), a.uniqueItems())
-			&& empty(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
+			allEmpty(a.description(), a._default(), a._enum(), a.example(), a.api())
+			&& allEmpty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.collectionFormat(), a.maximum(), a.minimum(), a.multipleOf())
+			&& allFalse(a.allowEmptyValue(), a.exclusiveMaximum(), a.exclusiveMinimum(), a.required(), a.uniqueItems())
+			&& allMinusOne(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
 			&& empty(a.items());
 	}
 	
@@ -479,7 +490,7 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.description(), a.example(), a.examples(), a.api())
+			allEmpty(a.description(), a.example(), a.examples(), a.api())
 			&& a.headers().length == 0
 			&& empty(a.schema())
 		;
@@ -495,10 +506,10 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return
-			empty(a.description(), a._default(), a._enum(), a.example(), a.api())
-			&& empty(a.name(), a.value(), a.type(), a.format(), a.collectionFormat(), a.$ref(), a.maximum(), a.minimum(), a.multipleOf())
-			&& empty(a.exclusiveMaximum(), a.exclusiveMinimum(), a.uniqueItems())
-			&& empty(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
+			allEmpty(a.description(), a._default(), a._enum(), a.example(), a.api())
+			&& allEmpty(a.name(), a.value(), a.type(), a.format(), a.collectionFormat(), a.$ref(), a.maximum(), a.minimum(), a.multipleOf())
+			&& allFalse(a.exclusiveMaximum(), a.exclusiveMinimum(), a.uniqueItems())
+			&& allMinusOne(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
 			&& empty(a.items());
 	}
 
@@ -512,7 +523,7 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return
-			empty(a.description(), a.api());
+			allEmpty(a.description(), a.api());
 	}
 
 	/**
@@ -525,10 +536,11 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.value(), a.description(), a._default(), a._enum(), a.allOf(), a.properties(), a.additionalProperties(), a.xml(), a.example(), a.examples())
-			&& empty(a.$ref(), a.format(), a.title(), a.multipleOf(), a.maximum(), a.minimum(), a.pattern(), a.maxProperties(), a.minProperties(), a.type(), a.discriminator())
-			&& empty(a.ignore(), a.exclusiveMaximum(), a.exclusiveMinimum(), a.readOnly(), a.required(), a.uniqueItems())
-			&& empty(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
+			allEmpty(a.value(), a.description(), a._default(), a._enum(), a.allOf(), a.properties(), a.additionalProperties(), a.xml(), a.example(), a.examples())
+			&& allEmpty(a.$ref(), a.format(), a.title(), a.multipleOf(), a.maximum(), a.minimum(), a.pattern(), a.type(), a.discriminator())
+			&& allMinusOne(a.maxProperties(), a.minProperties())
+			&& allFalse(a.ignore(), a.exclusiveMaximum(), a.exclusiveMinimum(), a.readOnly(), a.required(), a.uniqueItems())
+			&& allMinusOne(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
 			&& empty(a.items())
 			&& empty(a.externalDocs());
 	}
@@ -543,8 +555,8 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.value(), a.description()) 
-			&& empty(a.url());
+			allEmpty(a.value(), a.description()) 
+			&& allEmpty(a.url());
 	}
 	
 	/**
@@ -557,8 +569,8 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.description(), a.example(), a.examples(), a.value()) 
-			&& empty(a.required())
+			allEmpty(a.description(), a.example(), a.examples(), a.api(), a.value()) 
+			&& allFalse(a.required())
 			&& empty(a.schema());
 	}
 
@@ -572,8 +584,8 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.value())
-			&& empty(a.name(), a.url(), a.email());
+			allEmpty(a.value())
+			&& allEmpty(a.name(), a.url(), a.email());
 	}
 
 	/**
@@ -586,8 +598,8 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.value())
-			&& empty(a.name(), a.url());
+			allEmpty(a.value())
+			&& allEmpty(a.name(), a.url());
 	}
 
 	/**
@@ -600,10 +612,10 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return
-			empty(a.value(), a._default(), a._enum())
-			&& empty(a.type(), a.format(), a.collectionFormat(), a.pattern(), a.$ref(), a.maximum(), a.minimum(), a.multipleOf())
-			&& empty(a.exclusiveMaximum(), a.exclusiveMinimum(), a.uniqueItems())
-			&& empty(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
+			allEmpty(a.value(), a._default(), a._enum())
+			&& allEmpty(a.type(), a.format(), a.collectionFormat(), a.pattern(), a.$ref(), a.maximum(), a.minimum(), a.multipleOf())
+			&& allFalse(a.exclusiveMaximum(), a.exclusiveMinimum(), a.uniqueItems())
+			&& allMinusOne(a.maxLength(), a.minLength(), a.maxItems(), a.minItems())
 			&& empty(a.items());
 	}
 
@@ -617,10 +629,10 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return
-			empty(a.value(), a._default(), a._enum(), a.items())
-			&& empty(a.type(), a.format(), a.collectionFormat(), a.pattern(), a.$ref(), a.maximum(), a.minimum(), a.multipleOf())
-			&& empty(a.exclusiveMaximum(), a.exclusiveMinimum(), a.uniqueItems())
-			&& empty(a.maxLength(), a.minLength(), a.maxItems(), a.minItems());
+			allEmpty(a.value(), a._default(), a._enum(), a.items())
+			&& allEmpty(a.type(), a.format(), a.collectionFormat(), a.pattern(), a.$ref(), a.maximum(), a.minimum(), a.multipleOf())
+			&& allFalse(a.exclusiveMaximum(), a.exclusiveMinimum(), a.uniqueItems())
+			&& allMinusOne(a.maxLength(), a.minLength(), a.maxItems(), a.minItems());
 	}
 
 	/**
@@ -633,56 +645,92 @@ public class AnnotationUtils {
 		if (a == null)
 			return true;
 		return 
-			empty(a.description(), a._enum(), a.example(), a.api())
-			&& empty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.maximum(), a.minimum(), a.multipleOf())
-			&& empty(a.exclusiveMaximum(), a.exclusiveMinimum())
-			&& empty(a.maxLength(), a.minLength())
+			allEmpty(a.description(), a._enum(), a.example(), a.api())
+			&& allEmpty(a.name(), a.value(), a.type(), a.format(), a.pattern(), a.maximum(), a.minimum(), a.multipleOf())
+			&& allFalse(a.exclusiveMaximum(), a.exclusiveMinimum())
+			&& allMinusOne(a.maxLength(), a.minLength())
 			&& empty(a.items());
 	}
 
 	/**
-	 * Returns <jk>true</jk> if the specified annotation contains all default values.
+	 * Returns <jk>true</jk> if all the specified strings are empty or null.
 	 * 
-	 * @param a The annotation to check.
-	 * @return <jk>true</jk> if the specified annotation contains all default values.
+	 * @param strings The strings to test.
+	 * @return <jk>true</jk> if all the specified strings are empty or null.
 	 */
-	public static boolean empty(ResourceSwagger a) {
-		if (a == null)
-			return true;
-		return 
-			empty(a.version())
-			&& empty(a.title(), a.description(), a.value())
-			&& empty(a.contact())
-			&& empty(a.license())
-			&& empty(a.externalDocs())
-			&& a.tags().length == 0;
-	}
-
-	private static boolean empty(String...strings) {
+	protected static boolean allEmpty(String...strings) {
 		for (String s : strings)
-			if (! s.isEmpty())
+			if (s != null && ! s.isEmpty())
 				return false;
 		return true;
 	}
 
-	private static boolean empty(String[]...strings) {
+	/**
+	 * Returns <jk>true</jk> if all the specified strings are empty or null.
+	 * 
+	 * @param strings The strings to test.
+	 * @return <jk>true</jk> if all the specified strings are empty or null.
+	 */
+	protected static boolean allEmpty(String[]...strings) {
 		for (String[] s : strings)
-			if (s.length != 0)
+			if (s.length != 0 || ! allEmpty(s))
 				return false;
 		return true;
 	}
 
-	private static boolean empty(boolean...booleans) {
+	/**
+	 * Returns <jk>true</jk> if all the specified booleans are false.
+	 * 
+	 * @param booleans The booleans to test.
+	 * @return <jk>true</jk> if all the specified booleans are false.
+	 */
+	protected static boolean allFalse(boolean...booleans) {
 		for (boolean b : booleans)
 			if (b)
 				return false;
 		return true;
 	}
 
-	private static boolean empty(long...longs) {
+	/**
+	 * Returns <jk>true</jk> if all the specified longs are -1.
+	 * 
+	 * @param longs The booleans to test.
+	 * @return <jk>true</jk> if all the specified longs are -1.
+	 */
+	protected static boolean allMinusOne(long...longs) {
 		for (long i : longs)
 			if (i != -1)
 				return false;
 		return true;
 	}
+	
+	private static Set<String> toSet(String[] s) {
+		return toSet(joinnl(s));
+	}
+	
+	private static Set<String> toSet(String s) {
+		if (isEmpty(s))
+			return null;
+		Set<String> set = new ASet<>();
+		try {
+			for (Object o : StringUtils.parseListOrCdl(s))
+				set.add(o.toString());
+		} catch (ParseException e) {
+			throw new RuntimeException(e);
+		}
+		return set;
+	}
+	
+	final static ObjectMap toObjectMap(String[] ss) {
+		String s = joinnl(ss);
+		if (s.isEmpty())
+			return null;
+		if (! isObjectMap(s, true))
+			s = "{" + s + "}";
+		try {
+			return new ObjectMap(s);
+		} catch (ParseException e) {
+			throw new RuntimeException(e);
+		}
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Body.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Body.java
similarity index 84%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Body.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Body.java
index 2321d3a..d3d37e7 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Body.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Body.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -21,9 +21,12 @@ import java.nio.charset.*;
 import java.util.logging.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.httppart.oapi.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.jsonschema.*;
-import org.apache.juneau.rest.exception.*;
+import org.apache.juneau.remoteable.*;
+import org.apache.juneau.serializer.*;
 
 /**
  * REST request body annotation.
@@ -31,6 +34,18 @@ import org.apache.juneau.rest.exception.*;
  * <p>
  * Identifies a POJO to be used as the body of an HTTP request.
  * 
+ * <p>
+ * Can be used in the following locations:
+ * <ul>
+ * 	<li>Java method arguments of client-side REST interface proxies.
+ * 	<li>Java method arguments of server-side REST Java methods and/or their class types.
+ * </ul>
+ * 
+ * <h5 class='topic'>Server-side REST</h5>
+ * 
+ * <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.
+ * 
  * <h5 class='section'>Examples:</h5>
  * <p class='bcode w800'>
  * 	<jc>// Used on parameter</jc>
@@ -88,9 +103,9 @@ import org.apache.juneau.rest.exception.*;
  * This annotation can be applied to the following:
  * <ul class='spaced-list'>
  * 	<li>
- * 		Parameters on a {@link RestMethod @RestMethod}-annotated method.
+ * 		Parameters on a <ja>@RestMethod</ja>-annotated method.
  * 	<li>
- * 		POJO classes used as parameters on a {@link RestMethod @RestMethod}-annotated method.
+ * 		POJO classes used as parameters on a <ja>@RestMethod</ja>-annotated method.
  * </ul>
  * 
  * <p>
@@ -162,13 +177,66 @@ import org.apache.juneau.rest.exception.*;
  * 	<li class='link'><a class="doclink" href="../../../../../overview-summary.html#juneau-rest-server.Body">Overview &gt; juneau-rest-server &gt; @Body</a>
  * 	<li class='link'><a class="doclink" href="https://swagger.io/specification/v2/#parameterObject">Swagger Specification &gt; Parameter Object</a>
  * </ul>
+ * 
+ * <h5 class='topic'>Client-side REST</h5>
+ * 
+ * Annotation applied to Java method arguments of interface proxies to denote that they are the HTTP body of the request.
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+ * 		String myProxyMethod(<ja>@Body</ja> MyPojo pojo);
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The argument can be any of the following types:
+ * <ul class='spaced-list'>
+ * 	<li>
+ * 		Any serializable POJO - Converted to text using the {@link Serializer} registered with the
+ * 		<code>RestClient</code>.
+ * 	<li>
+ * 		{@link Reader} - Raw contents of {@code Reader} will be serialized to remote resource.
+ * 	<li>
+ * 		{@link InputStream} - Raw contents of {@code InputStream} will be serialized to remote resource.
+ * 	<li>
+ * 		<code>HttpEntity</code> - Bypass Juneau serialization and pass HttpEntity directly to HttpClient.
+ * 	<li>
+ * 		<code>NameValuePairs</code> - Converted to a URL-encoded FORM post.
+ * </ul>
+ * 
+ * <p>
+ * The annotation can also be applied to a bean property field or getter when the argument is annotated with
+ * {@link RequestBean @RequestBean}:
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+ * 		String myProxyMethod(<ja>@RequestBean</ja> MyRequestBean bean);
+ * 	}
+ * 
+ * 	<jk>public interface</jk> MyRequestBean {
+ * 		<ja>@Body</ja>
+ * 		MyPojo getMyPojo();
+ * 	}
+ * </p>
+ * 
+ * <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>
  */
 @Documented
-@Target({PARAMETER,TYPE})
+@Target({PARAMETER,FIELD,METHOD,TYPE})
 @Retention(RUNTIME)
 @Inherited
 public @interface Body {
-	
 	//=================================================================================================================
 	// Attributes common to all Swagger Parameter objects
 	//=================================================================================================================
@@ -216,7 +284,8 @@ public @interface Body {
 	 * Determines whether the body is mandatory.
 	 *  
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <h5 class='section'>Examples:</h5>
 	 * <p class='bcode'>
@@ -492,4 +561,18 @@ public @interface Body {
 	 * </ul>
 	 */
 	String[] value() default {};
+
+	/**
+	 * TODO
+	 */
+	String[] api() default {};
+
+	/**
+	 * Specifies the {@link HttpPartParser} class used for parsing values from strings.
+	 * 
+	 * <p>
+	 * The default value for this parser is inherited from the servlet/method which defaults to {@link OapiPartParser}.
+	 * <br>You can use {@link SimplePartParser} to parse POJOs that are directly convertible from <code>Strings</code>.
+	 */
+	Class<? extends HttpPartParser> parser() default HttpPartParser.Null.class;
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Contact.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Contact.java
similarity index 96%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Contact.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Contact.java
index 3f9458b..87b6fc0 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Contact.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Contact.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ExternalDocs.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ExternalDocs.java
similarity index 99%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ExternalDocs.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ExternalDocs.java
index 0b45080..977e310 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ExternalDocs.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ExternalDocs.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import org.apache.juneau.json.*;
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/FormData.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/FormData.java
similarity index 68%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/FormData.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/FormData.java
index 657a15f..5853541 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/FormData.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/FormData.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -22,11 +22,25 @@ import org.apache.juneau.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.httppart.oapi.*;
 import org.apache.juneau.json.*;
-import org.apache.juneau.rest.*;
-import org.apache.juneau.rest.exception.*;
+import org.apache.juneau.remoteable.*;
+import org.apache.juneau.urlencoding.*;
 
 /**
- * Annotation that can be applied to a parameter of a {@link RestMethod @RestMethod} annotated method to identify it as a form post
+  * REST request form-data annotation.
+ * 
+ * <p>
+ * Identifies a POJO to be used as a form-data entry on an HTTP request.
+ * 
+ * <p>
+ * Can be used in the following locations:
+ * <ul>
+ * 	<li>Java method arguments of client-side REST interface proxies.
+ * 	<li>Java method arguments of server-side REST Java methods and/or their class types.
+ * </ul>
+ * 
+ * <h5 class='topic'>Server-side REST</h5>
+ * 
+ * Annotation that can be applied to a parameter of a <ja>@RestMethod</ja>-annotated method to identify it as a form post
  * entry converted to a POJO.
  * 
  * <h5 class='section'>Example:</h5>
@@ -52,7 +66,7 @@ import org.apache.juneau.rest.exception.*;
  * 
  * <h5 class='topic'>Important note concerning FORM posts</h5>
  * 
- * This annotation should not be combined with the {@link Body @Body} annotation or {@link RestRequest#getBody()} method
+ * This annotation should not be combined with the {@link Body @Body} annotation or <code>RestRequest.getBody()</code> method
  * for <code>application/x-www-form-urlencoded POST</code> posts, since it will trigger the underlying servlet
  * API to parse the body content as key-value pairs resulting in empty content.
  * 
@@ -65,14 +79,206 @@ import org.apache.juneau.rest.exception.*;
  * 	<li class='link'><a class="doclink" href="../../../../../overview-summary.html#juneau-rest-server.FormData">Overview &gt; juneau-rest-server &gt; @FormData</a>
  * 	<li class='link'><a class="doclink" href="https://swagger.io/specification/v2/#parameterObject">Swagger Specification &gt; Parameter Object</a>
  * </ul>
+ * 
+ * <h5 class='topic'>Client-side REST</h5>
+* Annotation applied to Java method arguments of interface proxies to denote that they are FORM post parameters on the
+ * request.
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<jc>// Explicit names specified for form data parameters.</jc>
+ * 		<jc>// pojo will be converted to UON notation (unless plain-text parts enabled).</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod1"</js>)
+ * 		String myProxyMethod1(<ja>@FormData</ja>(<js>"foo"</js>)</ja> String foo,
+ * 			<ja>@FormData</ja>(<js>"bar"</js>)</ja> MyPojo pojo);
+ * 
+ * 		<jc>// Multiple values pulled from a NameValuePairs object.</jc>
+ * 		<jc>// Same as @FormData("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod2"</js>)
+ * 		String myProxyMethod2(<ja>@FormData</ja> NameValuePairs nameValuePairs);
+ * 
+ * 		<jc>// Multiple values pulled from a Map.</jc>
+ * 		<jc>// Same as @FormData("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod3"</js>)
+ * 		String myProxyMethod3(<ja>@FormData</ja> Map&lt;String,Object&gt; map);
+ * 
+ * 		<jc>// Multiple values pulled from a bean.</jc>
+ * 		<jc>// Same as @FormData("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod4"</js>)
+ * 		String myProxyMethod4(<ja>@FormData</ja> MyBean myBean);
+ * 
+ * 		<jc>// An entire form-data HTTP body as a String.</jc>
+ * 		<jc>// Same as @FormData("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod5"</js>)
+ * 		String myProxyMethod5(<ja>@FormData</ja> String string);
+ * 
+ * 		<jc>// An entire form-data HTTP body as a Reader.</jc>
+ * 		<jc>// Same as @FormData("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod6"</js>)
+ * 		String myProxyMethod6(<ja>@FormData</ja> Reader reader);
+ * 
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The annotation can also be applied to a bean property field or getter when the argument is annotated with
+ * {@link RequestBean @RequestBean}:
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+ * 		String myProxyMethod(<ja>@RequestBean</ja> MyRequestBean bean);
+ * 	}
+ * 
+ * 	<jk>public interface</jk> MyRequestBean {
+ * 
+ * 		<jc>// Name explicitly specified.</jc>
+ * 		<ja>@FormData</ja>(<js>"foo"</js>)
+ * 		String getX();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @FormData("bar")</jc>
+ * 		<ja>@FormData</ja>
+ * 		String getBar();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @FormData("baz")</jc>
+ * 		<ja>@FormData</ja>
+ * 		<ja>@BeanProperty</ja>(<js>"baz"</js>)
+ * 		String getY();
+ * 
+ * 		<jc>// Multiple values pulled from NameValuePairs object.</jc>
+ * 		<jc>// Same as @FormData("*")</jc>
+ * 		<ja>@FormData</ja>
+ * 		NameValuePairs getNameValuePairs();
+ * 
+ * 		<jc>// Multiple values pulled from Map.</jc>
+ * 		<jc>// Same as @FormData("*")</jc>
+ * 		<ja>@FormData</ja>
+ * 	 	Map&lt;String,Object&gt; getMap();
+ * 
+ * 		<jc>// Multiple values pulled from bean.</jc>
+ * 		<jc>// Same as @FormData("*")</jc>
+ * 		<ja>@FormData</ja>
+ * 	 	MyBean getMyBean();
+ * 
+ * 		<jc>// An entire form-data HTTP body as a Reader.</jc>
+ * 		<jc>// Same as @FormData("*")</jc>
+ * 		<ja>@FormData</ja>
+ * 		Reader getReader();
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The {@link #name()} and {@link #value()} elements are synonyms for specifying the parameter name.
+ * Only one should be used.
+ * <br>The following annotations are fully equivalent:
+ * <p class='bcode'>
+ * 	<ja>@FormData</ja>(name=<js>"foo"</js>)
+ * 
+ * 	<ja>@FormData</ja>(<js>"foo"</js>)
+ * </p>
+ * 
+ * <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>
  */
 @Documented
-@Target({PARAMETER,TYPE})
+@Target({PARAMETER,FIELD,METHOD,TYPE})
 @Retention(RUNTIME)
 @Inherited
 public @interface FormData {
 
 	/**
+	 * The form post parameter name.
+	 * 
+	 * <p>
+	 * Note that {@link #name()} and {@link #value()} are synonyms.
+	 * 
+	 * <p>
+	 * The value should be either <js>"*"</js> to represent multiple name/value pairs, or a label that defines the
+	 * form data parameter name.
+	 * 
+	 * <p>
+	 * A blank value (the default) has the following behavior:
+	 * <ul class='spaced-list'>
+	 * 	<li>
+	 * 		If the data type is <code>NameValuePairs</code>, <code>Map</code>, or a bean,
+	 * 		then it's the equivalent to <js>"*"</js> which will cause the value to be serialized as name/value pairs.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jc>// When used on a remote method parameter</jc>
+	 * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+	 * 	<jk>public interface</jk> MyProxy {
+	 * 
+	 * 		<jc>// Equivalent to @FormData("*")</jc>
+	 * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+	 * 		String myProxyMethod1(<ja>@FormData</ja> Map&lt;String,Object&gt; formData);
+	 * 	}
+	 * 
+	 * 	<jc>// When used on a request bean method</jc>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @FormData("*")</jc>
+	 * 		<ja>@FormData</ja>
+	 * 		Map&lt;String,Object&gt; getFoo();
+	 * 	}
+	 * 		</p>
+	 * 	</li>
+	 * 	<li>
+	 * 		If used on a request bean method, uses the bean property name.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @FormData("foo")</jc>
+	 * 		<ja>@FormData</ja>
+	 * 		String getFoo();
+	 * 	}
+	 * 		</p>
+	 * 	</li>
+	 * </ul>
+	 */
+//	String name() default "";
+
+	/**
+	 * A synonym for {@link #name()}.
+	 * 
+	 * <p>
+	 * Allows you to use shortened notation if you're only specifying the name.
+	 */
+//	String value() default "";
+
+	/**
+	 * Skips this value if it's an empty string or empty collection/array.
+	 * 
+	 * <p>
+	 * Note that <jk>null</jk> values are already ignored.
+	 */
+	boolean skipIfEmpty() default false;
+
+	/**
+	 * Specifies the {@link HttpPartSerializer} class used for serializing values to strings.
+	 * 
+	 * <p>
+	 * The default value defaults to the using the part serializer defined on the {@link RequestBean @RequestBean} annotation,
+	 * then on the client which by default is {@link UrlEncodingSerializer}.
+	 * 
+	 * <p>
+	 * This annotation is provided to allow values to be custom serialized.
+	 */
+	Class<? extends HttpPartSerializer> serializer() default HttpPartSerializer.Null.class;
+	
+	/**
 	 * Specifies the {@link HttpPartParser} class used for parsing values from strings.
 	 * 
 	 * <p>
@@ -141,7 +347,8 @@ public @interface FormData {
 	 * Determines whether the parameter is mandatory.
 	 *  
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 */
 	boolean required() default false;
 	
@@ -331,7 +538,8 @@ public @interface FormData {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -345,7 +553,8 @@ public @interface FormData {
 	 * Defines whether the maximum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -361,7 +570,8 @@ public @interface FormData {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -375,7 +585,8 @@ public @interface FormData {
 	 * Defines whether the minimum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -392,7 +603,8 @@ public @interface FormData {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -408,7 +620,8 @@ public @interface FormData {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -422,7 +635,8 @@ public @interface FormData {
 	 * A string input is valid if it matches the specified regular expression pattern.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -436,7 +650,8 @@ public @interface FormData {
 	 * An array or collection is valid if its size is less than, or equal to, the value of this keyword.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"array"</js>.
@@ -450,7 +665,8 @@ public @interface FormData {
 	 * An array or collection is valid if its size is greater than, or equal to, the value of this keyword.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"array"</js>.
@@ -464,7 +680,8 @@ public @interface FormData {
 	 * If <jk>true</jk>, the input validates successfully if all of its elements are unique.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * <br>If the parameter type is a subclass of {@link Set}, this validation is skipped (since a set can only contain unique items anyway).
 	 * <br>Otherwise, the collection or array is checked for duplicate items.
 	 * 
@@ -480,7 +697,8 @@ public @interface FormData {
 	 * If specified, the input validates successfully if it is equal to one of the elements in this array.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * The format is a {@link JsonSerializer#DEFAULT_LAX Simple-JSON} array or comma-delimited list.
@@ -514,9 +732,9 @@ public @interface FormData {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
-	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
 	 */
 	String multipleOf() default "";
@@ -529,7 +747,7 @@ public @interface FormData {
 	 * A serialized example of the parameter.
 	 * 
 	 * <p>
-	 * This attribute defines a JSON representation of the value that is used by {@link BasicRestInfoProvider} to construct
+	 * This attribute defines a JSON representation of the value that is used by <code>BasicRestInfoProvider</code> to construct
 	 * an example of the form data entry.
 	 * 
 	 * <h5 class='section'>Notes:</h5>
@@ -616,4 +834,5 @@ public @interface FormData {
 	 * </ul>
 	 */
 	String[] api() default {};
+	
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HasFormData.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/HasFormData.java
similarity index 92%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HasFormData.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/HasFormData.java
index 498b125..009fdd8 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HasFormData.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/HasFormData.java
@@ -10,17 +10,15 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
 
 import java.lang.annotation.*;
 
-import org.apache.juneau.rest.*;
-
 /**
- * Annotation that can be applied to a parameter of a {@link RestMethod @RestMethod} annotated method to identify whether or not
+ * Annotation that can be applied to a parameter of a <ja>@RestMethod</ja> annotated method to identify whether or not
  * the request has the specified multipart form POST parameter.
  * 
  * <p>
@@ -76,7 +74,7 @@ import org.apache.juneau.rest.*;
  * 
  * <h5 class='topic'>Important note concerning FORM posts</h5>
  * 
- * This annotation should not be combined with the {@link Body @Body} annotation or {@link RestRequest#getBody()} method
+ * This annotation should not be combined with the {@link Body @Body} annotation or <code>RestRequest.getBody()</code> method
  * for <code>application/x-www-form-urlencoded POST</code> posts, since it will trigger the underlying servlet API to
  * parse the body content as key-value pairs, resulting in empty content.
  * 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HasQuery.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/HasQuery.java
similarity index 93%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HasQuery.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/HasQuery.java
index c1e4664..9187b89 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HasQuery.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/HasQuery.java
@@ -10,15 +10,13 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
 
 import java.lang.annotation.*;
 
-import org.apache.juneau.rest.*;
-
 /**
  * Identical to {@link HasFormData @HasFormData}, but only checks the existing of the parameter in the URL string, not
  * URL-encoded form posts.
@@ -27,7 +25,7 @@ import org.apache.juneau.rest.*;
  * Unlike {@link HasFormData @HasFormData}, using this annotation does not result in the servlet reading the contents
  * of URL-encoded form posts.
  * Therefore, this annotation can be used in conjunction with the {@link Body @Body} annotation or
- * {@link RestRequest#getBody()} method for <code>application/x-www-form-urlencoded POST</code> calls.
+ * <code>RestRequest.getBody()</code> method for <code>application/x-www-form-urlencoded POST</code> calls.
  * 
  * <h5 class='section'>Example:</h5>
  * <p class='bcode'>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Header.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Header.java
similarity index 67%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Header.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Header.java
index dcae57e..656ffa8 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Header.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Header.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -22,11 +22,25 @@ import org.apache.juneau.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.httppart.oapi.*;
 import org.apache.juneau.json.*;
-import org.apache.juneau.rest.*;
-import org.apache.juneau.rest.exception.*;
+import org.apache.juneau.remoteable.*;
+import org.apache.juneau.urlencoding.*;
 
 /**
- * Annotation that can be applied to a parameter of a {@link RestMethod @RestMethod} annotated method to identify it as a HTTP
+ * REST request body annotation.
+ * 
+ * <p>
+ * Identifies a POJO to be used as the header of an HTTP request.
+ * 
+ * <p>
+ * Can be used in the following locations:
+ * <ul>
+ * 	<li>Java method arguments of client-side REST interface proxies.
+ * 	<li>Java method arguments of server-side REST Java methods and/or their class types.
+ * </ul>
+ * 
+ * <h5 class='topic'>Server-side REST</h5>
+ * 
+ * Annotation that can be applied to a parameter of a <ja>@RestMethod</ja>-annotated method to identify it as a HTTP
  * request header converted to a POJO.
  * 
  * <h5 class='section'>Example:</h5>
@@ -52,14 +66,191 @@ import org.apache.juneau.rest.exception.*;
  * 	<li class='link'><a class="doclink" href="../../../../../overview-summary.html#juneau-rest-server.Header">Overview &gt; juneau-rest-server &gt; @Header</a>
  * 	<li class='link'><a class="doclink" href="https://swagger.io/specification/v2/#parameterObject">Swagger Specification &gt; Parameter Object</a>
  * </ul>
+ * 
+ * <h5 class='topic'>Client-side REST</h5>
+ * 
+ * Annotation applied to Java method arguments of interface proxies to denote that they are serialized as an HTTP
+ * header value.
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<jc>// Explicit names specified for HTTP headers.</jc>
+ * 		<jc>// pojo will be converted to UON notation (unless plain-text parts enabled).</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod1"</js>)
+ * 		String myProxyMethod1(<ja>@Header</ja>(<js>"Foo"</js>)</ja> String foo,
+ * 			<ja>@Header</ja>(<js>"Bar"</js>)</ja> MyPojo pojo);
+ * 
+ * 		<jc>// Multiple values pulled from a NameValuePairs object.</jc>
+ * 		<jc>// Same as @Header("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod2"</js>)
+ * 		String myProxyMethod2(<ja>@Header</ja> NameValuePairs nameValuePairs);
+ * 
+ * 		<jc>// Multiple values pulled from a Map.</jc>
+ * 		<jc>// Same as @Header("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod3"</js>)
+ * 		String myProxyMethod3(<ja>@Header</ja> Map&lt;String,Object&gt; map);
+ * 
+ * 		<jc>// Multiple values pulled from a bean.</jc>
+ * 		<jc>// Same as @Header("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod4"</js>)
+ * 		String myProxyMethod4(<ja>@Header</ja> MyBean myBean);
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The annotation can also be applied to a bean property field or getter when the argument is annotated with
+ * {@link RequestBean @RequestBean}:
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+ * 		String myProxyMethod(<ja>@RequestBean</ja> MyRequestBean bean);
+ * 	}
+ * 
+ * 	<jk>public interface</jk> MyRequestBean {
+ * 
+ * 		<jc>// Name explicitly specified.</jc>
+ * 		<ja>@Header</ja>(<js>"Foo"</js>)
+ * 		String getX();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @Header("bar")</jc>
+ * 		<ja>@Header</ja>
+ * 		String getBar();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @Header("Baz")</jc>
+ * 		<ja>@Header</ja>
+ * 		<ja>@BeanProperty</ja>(<js>"Baz"</js>)
+ * 		String getY();
+ * 
+ * 		<jc>// Multiple values pulled from NameValuePairs object.</jc>
+ * 		<jc>// Same as @Header("*")</jc>
+ * 		<ja>@Header</ja>
+ * 		NameValuePairs getNameValuePairs();
+ * 
+ * 		<jc>// Multiple values pulled from Map.</jc>
+ * 		<jc>// Same as @Header("*")</jc>
+ * 		<ja>@Header</ja>
+ * 	 	Map&lt;String,Object&gt; getMap();
+ * 
+ * 		<jc>// Multiple values pulled from bean.</jc>
+ * 		<jc>// Same as @Header("*")</jc>
+ * 		<ja>@Header</ja>
+ * 	 	MyBean getBean();
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The {@link #name()} and {@link #value()} elements are synonyms for specifying the header name.
+ * Only one should be used.
+ * <br>The following annotations are fully equivalent:
+ * <p class='bcode'>
+ * 	<ja>@Header</ja>(name=<js>"Foo"</js>)
+ * 
+ * 	<ja>@Header</ja>(<js>"Foo"</js>)
+ * </p>
+ * 
+ * <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>
  */
 @Documented
-@Target({PARAMETER,TYPE})
+@Target({PARAMETER,FIELD,METHOD,TYPE})
 @Retention(RUNTIME)
 @Inherited
 public @interface Header {
 
 	/**
+	 * The HTTP header name.
+	 * 
+	 * <p>
+	 * A blank value (the default) indicates to reuse the bean property name when used on a request bean property.
+	 * 
+	 * <p>
+	 * The value should be either <js>"*"</js> to represent multiple name/value pairs, or a label that defines the
+	 * header name.
+	 * 
+	 * <p>
+	 * A blank value (the default) has the following behavior:
+	 * <ul class='spaced-list'>
+	 * 	<li>
+	 * 		If the data type is <code>NameValuePairs</code>, <code>Map</code>, or a bean,
+	 * 		then it's the equivalent to <js>"*"</js> which will cause the value to be serialized as name/value pairs.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jc>// When used on a remote method parameter</jc>
+	 * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+	 * 	<jk>public interface</jk> MyProxy {
+	 * 
+	 * 		<jc>// Equivalent to @Header("*")</jc>
+	 * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+	 * 		String myProxyMethod1(<ja>@Header</ja> Map&lt;String,Object&gt; headers);
+	 * 	}
+	 * 
+	 * 	<jc>// When used on a request bean method</jc>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @Header("*")</jc>
+	 * 		<ja>@Header</ja>
+	 * 		Map&lt;String,Object&gt; getFoo();
+	 * 	}
+	 * 		</p>
+	 * 	</li>
+	 * 	<li>
+	 * 		If used on a request bean method, uses the bean property name.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @Header("Foo")</jc>
+	 * 		<ja>@Header</ja>
+	 * 		<ja>@BeanProperty</ja>(<js>"Foo"</js>)
+	 * 		String getFoo();
+	 * 	}
+	 * 		</p>
+	 * 	</li>
+	 * </ul>
+	 */
+//	String name() default "";
+
+	/**
+	 * A synonym for {@link #name()}.
+	 * 
+	 * <p>
+	 * Allows you to use shortened notation if you're only specifying the name.
+	 */
+//	String value() default "";
+
+	/**
+	 * Skips this value if it's an empty string or empty collection/array.
+	 * 
+	 * <p>
+	 * Note that <jk>null</jk> values are already ignored.
+	 */
+	boolean skipIfEmpty() default false;
+
+	/**
+	 * Specifies the {@link HttpPartSerializer} class used for serializing values to strings.
+	 * 
+	 * <p>
+	 * The default value defaults to the using the part serializer defined on the {@link RequestBean @RequestBean} annotation,
+	 * then on the client which by default is {@link UrlEncodingSerializer}.
+	 * 
+	 * <p>
+	 * This annotation is provided to allow values to be custom serialized.
+	 */
+	Class<? extends HttpPartSerializer> serializer() default HttpPartSerializer.Null.class;
+	/**
 	 * Specifies the {@link HttpPartParser} class used for parsing values from strings.
 	 * 
 	 * <p>
@@ -120,7 +311,7 @@ public @interface Header {
 	 * Determines whether the parameter is mandatory.
 	 *  
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 */
 	boolean required() default false;
 	
@@ -224,6 +415,18 @@ public @interface Header {
 	 */
 	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.
 	 * 
@@ -298,7 +501,8 @@ public @interface Header {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -312,7 +516,8 @@ public @interface Header {
 	 * Defines whether the maximum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -328,7 +533,8 @@ public @interface Header {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -342,7 +548,8 @@ public @interface Header {
 	 * Defines whether the minimum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -359,7 +566,8 @@ public @interface Header {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -375,7 +583,8 @@ public @interface Header {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -389,7 +598,8 @@ public @interface Header {
 	 * A string input is valid if it matches the specified regular expression pattern.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -403,7 +613,8 @@ public @interface Header {
 	 * An array or collection is valid if its size is less than, or equal to, the value of this keyword.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"array"</js>.
@@ -417,7 +628,8 @@ public @interface Header {
 	 * An array or collection is valid if its size is greater than, or equal to, the value of this keyword.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"array"</js>.
@@ -431,7 +643,8 @@ public @interface Header {
 	 * If <jk>true</jk> the input validates successfully if all of its elements are unique.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * <br>If the parameter type is a subclass of {@link Set}, this validation is skipped (since a set can only contain unique items anyway).
 	 * <br>Otherwise, the collection or array is checked for duplicate items.
 	 * 
@@ -447,7 +660,8 @@ public @interface Header {
 	 * If specified, the input validates successfully if it is equal to one of the elements in this array.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * The format is a {@link JsonSerializer#DEFAULT_LAX Simple-JSON} array or comma-delimited list.
@@ -481,7 +695,8 @@ public @interface Header {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -496,7 +711,7 @@ public @interface Header {
 	 * A serialized example of the parameter.
 	 * 
 	 * <p>
-	 * This attribute defines a JSON representation of the value that is used by {@link BasicRestInfoProvider} to construct
+	 * This attribute defines a JSON representation of the value that is used by <code>BasicRestInfoProvider</code> to construct
 	 * an example of the header entry.
 	 * 
 	 * <h5 class='section'>Notes:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Items.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Items.java
similarity index 97%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Items.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Items.java
index 1e46e18..b3517dc 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Items.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Items.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/License.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/License.java
similarity index 97%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/License.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/License.java
index 0357834..a0da2ee 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/License.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/License.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Path.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
similarity index 68%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Path.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
index 9093e06..01d020b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Path.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Path.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -21,11 +21,25 @@ import org.apache.juneau.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.httppart.oapi.*;
 import org.apache.juneau.json.*;
-import org.apache.juneau.rest.*;
-import org.apache.juneau.rest.exception.*;
+import org.apache.juneau.remoteable.*;
+import org.apache.juneau.urlencoding.*;
 
 /**
- * Annotation that can be applied to a parameter of a {@link RestMethod @RestMethod} annotated method to identify it as a variable
+  * REST request path annotation.
+ * 
+ * <p>
+ * Identifies a POJO to be used as a patn entry on an HTTP request.
+ * 
+ * <p>
+ * Can be used in the following locations:
+ * <ul>
+ * 	<li>Java method arguments of client-side REST interface proxies.
+ * 	<li>Java method arguments of server-side REST Java methods and/or their class types.
+ * </ul>
+ * 
+ * <h5 class='topic'>Server-side REST</h5>
+ * 
+ * Annotation that can be applied to a parameter of a <ja>@RestMethod</ja>-annotated method to identify it as a variable
  * in a URL path pattern converted to a POJO.
  * 
  * <h5 class='section'>Example:</h5>
@@ -42,14 +56,179 @@ import org.apache.juneau.rest.exception.*;
  * 	<li class='link'><a class="doclink" href="../../../../../overview-summary.html#juneau-rest-server.MethodParameters">Overview &gt; juneau-rest-server &gt; Method Parameters</a>
  * 	<li class='link'><a class="doclink" href="https://swagger.io/specification/v2/#parameterObject">Swagger Specification &gt; Parameter Object</a>
  * </ul>
+ * 
+ * <h5 class='topic'>Client-side REST</h5>
+ * 
+ * Annotation applied to Java method arguments of interface proxies to denote that they are path variables on the request.
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<jc>// Explicit names specified for path parameters.</jc>
+ * 		<jc>// pojo will be converted to UON notation (unless plain-text parts enabled).</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod1/{foo}/{bar}"</js>)
+ * 		String myProxyMethod1(<ja>@Path</ja>(<js>"foo"</js>)</ja> String foo, <ja>@Path</ja>(<js>"bar"</js>)</ja> MyPojo pojo);
+ * 
+ * 		<jc>// Multiple values pulled from a NameValuePairs object.</jc>
+ * 		<jc>// Same as @Path("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod2/{foo}/{bar}/{baz}"</js>)
+ * 		String myProxyMethod2(<ja>@Path</ja> NameValuePairs nameValuePairs);
+ * 
+ * 		<jc>// Multiple values pulled from a Map.</jc>
+ * 		<jc>// Same as @Path("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod3/{foo}/{bar}/{baz}"</js>)
+ * 		String myProxyMethod3(<ja>@Path</ja> Map&lt;String,Object&gt; map);
+ * 
+ * 		<jc>// Multiple values pulled from a bean.</jc>
+ * 		<jc>// Same as @Path("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod4/{foo}/{bar}/{baz}"</js>)
+ * 		String myProxyMethod4(<ja>@Path</ja> MyBean myBean);
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The annotation can also be applied to a bean property field or getter when the argument is annotated with
+ * {@link RequestBean @RequestBean}:
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod/{foo}/{bar}/{baz}"</js>)
+ * 		String myProxyMethod(<ja>@RequestBean</ja> MyRequestBean bean);
+ * 	}
+ * 
+ * 	<jk>public interface</jk> MyRequestBean {
+ * 
+ * 		<jc>// Name explicitly specified.</jc>
+ * 		<ja>@Path</ja>(<js>"foo"</js>)
+ * 		String getX();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @Path("bar")</jc>
+ * 		<ja>@Path</ja>
+ * 		String getBar();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @Path("baz")</jc>
+ * 		<ja>@Path</ja>
+ * 		<ja>@BeanProperty</ja>(<js>"baz"</js>)
+ * 		String getY();
+ * 
+ * 		<jc>// Multiple values pulled from NameValuePairs object.</jc>
+ * 		<jc>// Same as @Path("*")</jc>
+ * 		<ja>@Path</ja>
+ * 		NameValuePairs getNameValuePairs();
+ * 
+ * 		<jc>// Multiple values pulled from Map.</jc>
+ * 		<jc>// Same as @Path("*")</jc>
+ * 		<ja>@Path</ja>
+ * 	 	Map&lt;String,Object&gt; getMap();
+ * 
+ * 		<jc>// Multiple values pulled from bean.</jc>
+ * 		<jc>// Same as @Path("*")</jc>
+ * 		<ja>@Path</ja>
+ * 	 	MyBean getMyBean();
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The {@link #name()} and {@link #value()} elements are synonyms for specifying the path variable name.
+ * Only one should be used.
+ * <br>The following annotations are fully equivalent:
+ * <p class='bcode'>
+ * 	<ja>@Path</ja>(name=<js>"foo"</js>)
+ * 
+ * 	<ja>@Path</ja>(<js>"foo"</js>)
+ * </p>
+ * 
+ * <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>
  */
 @Documented
-@Target({PARAMETER,TYPE})
+@Target({PARAMETER,FIELD,METHOD,TYPE})
 @Retention(RUNTIME)
 @Inherited
 public @interface Path {
 
 	/**
+	 * The path parameter name.
+	 * 
+	 * <p>
+	 * Note that {@link #name()} and {@link #value()} are synonyms.
+	 * 
+	 * <p>
+	 * The value should be either <js>"*"</js> to represent multiple name/value pairs, or a label that defines the
+	 * path variable name.
+	 * 
+	 * <p>
+	 * A blank value (the default) has the following behavior:
+	 * <ul class='spaced-list'>
+	 * 	<li>
+	 * 		If the data type is <code>NameValuePairs</code>, <code>Map</code>, or a bean,
+	 * 		then it's the equivalent to <js>"*"</js> which will cause the value to be treated as name/value pairs.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jc>// When used on a remote method parameter</jc>
+	 * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+	 * 	<jk>public interface</jk> MyProxy {
+	 * 
+	 * 		<jc>// Equivalent to @Path("*")</jc>
+	 * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod/{foo}/{bar}"</js>)
+	 * 		String myProxyMethod1(<ja>@FormData</ja> Map&lt;String,Object&gt; pathVars);
+	 * 	}
+	 * 
+	 * 	<jc>// When used on a request bean method</jc>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @Path("*")</jc>
+	 * 		<ja>@Path</ja>
+	 * 		Map&lt;String,Object&gt; getPathVars();
+	 * 	}
+	 * 		</p>
+	 * 	</li>
+	 * 	<li>
+	 * 		If used on a request bean method, uses the bean property name.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @Path("foo")</jc>
+	 * 		<ja>@Path</ja>
+	 * 		String getFoo();
+	 * 	}
+	 * </ul>
+	 */
+//	String name() default "";
+
+	/**
+	 * A synonym for {@link #name()}.
+	 * 
+	 * <p>
+	 * Allows you to use shortened notation if you're only specifying the name.
+	 */
+//	String value() default "";
+
+	/**
+	 * Specifies the {@link HttpPartSerializer} class used for serializing values to strings.
+	 * 
+	 * <p>
+	 * The default value defaults to the using the part serializer defined on the {@link RequestBean @RequestBean} annotation,
+	 * then on the client which by default is {@link UrlEncodingSerializer}.
+	 * 
+	 * <p>
+	 * This annotation is provided to allow values to be custom serialized.
+	 */
+	Class<? extends HttpPartSerializer> serializer() default HttpPartSerializer.Null.class;
+	
+	/**
 	 * Specifies the {@link HttpPartParser} class used for parsing values from strings.
 	 * 
 	 * <p>
@@ -269,7 +448,8 @@ public @interface Path {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -283,7 +463,8 @@ public @interface Path {
 	 * Defines whether the maximum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -299,7 +480,8 @@ public @interface Path {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -313,7 +495,8 @@ public @interface Path {
 	 * Defines whether the minimum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -330,7 +513,8 @@ public @interface Path {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -346,7 +530,8 @@ public @interface Path {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -360,7 +545,8 @@ public @interface Path {
 	 * A string input is valid if it matches the specified regular expression pattern.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -374,7 +560,8 @@ public @interface Path {
 	 * If specified, the input validates successfully if it is equal to one of the elements in this array.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * The format is a {@link JsonSerializer#DEFAULT_LAX Simple-JSON} array or comma-delimited list.
@@ -410,7 +597,8 @@ public @interface Path {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -425,7 +613,7 @@ public @interface Path {
 	 * A serialized example of the parameter.
 	 * 
 	 * <p>
-	 * This attribute defines a JSON representation of the value that is used by {@link BasicRestInfoProvider} to construct
+	 * This attribute defines a JSON representation of the value that is used by <code>BasicRestInfoProvider</code> to construct
 	 * an example of the path.
 	 * 
 	 * <h5 class='section'>Notes:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/PathRemainder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainder.java
similarity index 92%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/PathRemainder.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainder.java
index ea80007..26e37be 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/PathRemainder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/PathRemainder.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -18,7 +18,7 @@ import static java.lang.annotation.RetentionPolicy.*;
 import java.lang.annotation.*;
 
 /**
- * Annotation that can be applied to a parameter of a {@link RestMethod @RestMethod} annotated method to identify it as the URL
+ * Annotation that can be applied to a parameter of a <ja>@RestMethod</ja>-annotated method to identify it as the URL
  * parameter remainder after a path pattern match.
  * 
  * <h5 class='section'>Example:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Query.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Query.java
similarity index 68%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Query.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Query.java
index 9579472..ecda835 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Query.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Query.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -22,10 +22,24 @@ import org.apache.juneau.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.httppart.oapi.*;
 import org.apache.juneau.json.*;
-import org.apache.juneau.rest.*;
-import org.apache.juneau.rest.exception.*;
+import org.apache.juneau.remoteable.*;
+import org.apache.juneau.urlencoding.*;
 
 /**
+  * REST request form-data annotation.
+ * 
+ * <p>
+ * Identifies a POJO to be used as a form-data entry on an HTTP request.
+ * 
+ * <p>
+ * Can be used in the following locations:
+ * <ul>
+ * 	<li>Java method arguments of client-side REST interface proxies.
+ * 	<li>Java method arguments of server-side REST Java methods and/or their class types.
+ * </ul>
+ * 
+ * <h5 class='topic'>Server-side REST</h5>
+ * 
  * Identical to {@link FormData @FormData}, but only retrieves the parameter from the URL string, not URL-encoded form
  * posts.
  * 
@@ -33,7 +47,7 @@ import org.apache.juneau.rest.exception.*;
  * Unlike {@link FormData @FormData}, using this annotation does not result in the servlet reading the contents of
  * URL-encoded form posts.
  * Therefore, this annotation can be used in conjunction with the {@link Body @Body} annotation or
- * {@link RestRequest#getBody()} method for <code>application/x-www-form-urlencoded POST</code> calls.
+ * <code>RestRequest.getBody()</code> method for <code>application/x-www-form-urlencoded POST</code> calls.
  * 
  * <h5 class='section'>Example:</h5>
  * <p class='bcode'>
@@ -61,14 +75,206 @@ import org.apache.juneau.rest.exception.*;
  * 	<li class='link'><a class="doclink" href="../../../../../overview-summary.html#juneau-rest-server.Query">Overview &gt; juneau-rest-server &gt; @Query</a>
  * 	<li class='link'><a class="doclink" href="https://swagger.io/specification/v2/#parameterObject">Swagger Specification &gt; Parameter Object</a>
  * </ul>
+ * 
+ * <h5 class='topic'>Client-side REST</h5>
+ * 
+ * Annotation applied to Java method arguments of interface proxies to denote that they are QUERY parameters on the
+ * request.
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<jc>// Explicit names specified for query parameters.</jc>
+ * 		<jc>// pojo will be converted to UON notation (unless plain-text parts enabled).</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod1"</js>)
+ * 		String myProxyMethod1(<ja>@Query</ja>(<js>"foo"</js>)</ja> String foo,
+ * 			<ja>@Query</ja>(<js>"bar"</js>)</ja> MyPojo pojo);
+ * 
+ * 		<jc>// Multiple values pulled from a NameValuePairs object.</jc>
+ * 		<jc>// Same as @Query("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod2"</js>)
+ * 		String myProxyMethod2(<ja>@Query</ja> NameValuePairs nameValuePairs);
+ * 
+ * 		<jc>// Multiple values pulled from a Map.</jc>
+ * 		<jc>// Same as @Query("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod3"</js>)
+ * 		String myProxyMethod3(<ja>@Query</ja> Map&lt;String,Object&gt; map);
+ * 
+ * 		<jc>// Multiple values pulled from a bean.</jc>
+ * 		<jc>// Same as @Query("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod4"</js>)
+ * 		String myProxyMethod4(<ja>@Query</ja> MyBean myBean);
+ * 
+ * 		<jc>// An entire query string as a String.</jc>
+ * 		<jc>// Same as @FQuery("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod5"</js>)
+ * 		String myProxyMethod5(<ja>@Query</ja> String string);
+ * 
+ * 		<jc>// An entire query string as a Reader.</jc>
+ * 		<jc>// Same as @Query("*").</jc>
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod6"</js>)
+ * 		String myProxyMethod6(<ja>@Query</ja> Reader reader);
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The annotation can also be applied to a bean property field or getter when the argument is annotated with
+ * {@link RequestBean @RequestBean}:
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+ * 	<jk>public interface</jk> MyProxy {
+ * 
+ * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+ * 		String myProxyMethod(<ja>@RequestBean</ja> MyRequestBean bean);
+ * 	}
+ * 
+ * 	<jk>public interface</jk> MyRequestBean {
+ * 
+ * 		<jc>// Name explicitly specified.</jc>
+ * 		<ja>@Query</ja>(<js>"foo"</js>)
+ * 		String getX();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @Query("bar")</jc>
+ * 		<ja>@Query</ja>
+ * 		String getBar();
+ * 
+ * 		<jc>// Name inherited from bean property.</jc>
+ * 		<jc>// Same as @Query("baz")</jc>
+ * 		<ja>@Query</ja>
+ * 		<ja>@BeanProperty</ja>(<js>"baz"</js>)
+ * 		String getY();
+ * 
+ * 		<jc>// Multiple values pulled from NameValuePairs object.</jc>
+ * 		<jc>// Same as @Query("*")</jc>
+ * 		<ja>@Query</ja>
+ * 		NameValuePairs getNameValuePairs();
+ * 
+ * 		<jc>// Multiple values pulled from Map.</jc>
+ * 		<jc>// Same as @Query("*")</jc>
+ * 		<ja>@Query</ja>
+ * 	 	Map&lt;String,Object&gt; getMap();
+ * 
+ * 		<jc>// Multiple values pulled from bean.</jc>
+ * 		<jc>// Same as @Query("*")</jc>
+ * 		<ja>@Query</ja>
+ * 	 	MyBean getMyBean();
+ * 
+ * 		<jc>// An entire query string as a Reader.</jc>
+ * 		<jc>// Same as @Query("*")</jc>
+ * 		<ja>@Query</ja>
+ * 		Reader getReader();
+ * 	}
+ * </p>
+ * 
+ * <p>
+ * The {@link #name()} and {@link #value()} elements are synonyms for specifying the parameter name.
+ * Only one should be used.
+ * <br>The following annotations are fully equivalent:
+ * <p class='bcode'>
+ * 	<ja>@Query</ja>(name=<js>"foo"</js>)
+ * 
+ * 	<ja>@Query</ja>(<js>"foo"</js>)
+ * </p>
+ * 
+ * <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>
  */
 @Documented
-@Target({PARAMETER,TYPE})
+@Target({PARAMETER,FIELD,METHOD,TYPE})
 @Retention(RUNTIME)
 @Inherited
 public @interface Query {
 
 	/**
+	 * The query parameter name.
+	 * 
+	 * <p>
+	 * Note that {@link #name()} and {@link #value()} are synonyms.
+	 * 
+	 * <p>
+	 * The value should be either <js>"*"</js> to represent multiple name/value pairs, or a label that defines the
+	 * query parameter name.
+	 * 
+	 * <p>
+	 * A blank value (the default) has the following behavior:
+	 * <ul class='spaced-list'>
+	 * 	<li>
+	 * 		If the data type is <code>NameValuePairs</code>, <code>Map</code>, or a bean,
+	 * 		then it's the equivalent to <js>"*"</js> which will cause the value to be serialized as name/value pairs.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jc>// When used on a remote method parameter</jc>
+	 * 	<ja>@Remoteable</ja>(path=<js>"/myproxy"</js>)
+	 * 	<jk>public interface</jk> MyProxy {
+	 * 
+	 * 		<jc>// Equivalent to @Query("*")</jc>
+	 * 		<ja>@RemoteMethod</ja>(path=<js>"/mymethod"</js>)
+	 * 		String myProxyMethod1(<ja>@Query</ja> Map&lt;String,Object&gt; formData);
+	 * 	}
+	 * 
+	 * 	<jc>// When used on a request bean method</jc>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @Query("*")</jc>
+	 * 		<ja>@Query</ja>
+	 * 		Map&lt;String,Object&gt; getFoo();
+	 * 	}
+	 * 		</p>
+	 * 	</li>
+	 * 	<li>
+	 * 		If used on a request bean method, uses the bean property name.
+	 * 
+	 * 		<h5 class='figure'>Example:</h5>
+	 * 		<p class='bcode'>
+	 * 	<jk>public interface</jk> MyRequestBean {
+	 * 
+	 * 		<jc>// Equivalent to @Query("foo")</jc>
+	 * 		<ja>@Query</ja>
+	 * 		String getFoo();
+	 * 	}
+	 * 		</p>
+	 * 	</li>
+	 * </ul>
+	 */
+//	String name() default "";
+
+	/**
+	 * A synonym for {@link #name()}.
+	 * 
+	 * <p>
+	 * Allows you to use shortened notation if you're only specifying the name.
+	 */
+//	String value() default "";
+
+	/**
+	 * Skips this value if it's an empty string or empty collection/array.
+	 * 
+	 * <p>
+	 * Note that <jk>null</jk> values are already ignored.
+	 */
+	boolean skipIfEmpty() default false;
+
+	/**
+	 * Specifies the {@link HttpPartSerializer} class used for serializing values to strings.
+	 * 
+	 * <p>
+	 * The default value defaults to the using the part serializer defined on the {@link RequestBean @RequestBean} annotation,
+	 * then on the client which by default is {@link UrlEncodingSerializer}.
+	 * 
+	 * <p>
+	 * This annotation is provided to allow values to be custom serialized.
+	 */
+	Class<? extends HttpPartSerializer> serializer() default HttpPartSerializer.Null.class;
+
+	/**
 	 * Specifies the {@link HttpPartParser} class used for parsing values from strings.
 	 * 
 	 * <p>
@@ -137,7 +343,8 @@ public @interface Query {
 	 * Determines whether the parameter is mandatory.
 	 *  
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 */
 	boolean required() default false;
 	
@@ -325,7 +532,8 @@ public @interface Query {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -339,7 +547,8 @@ public @interface Query {
 	 * Defines whether the maximum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -355,7 +564,8 @@ public @interface Query {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -369,7 +579,8 @@ public @interface Query {
 	 * Defines whether the minimum is matched exclusively.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -386,7 +597,8 @@ public @interface Query {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -402,7 +614,8 @@ public @interface Query {
 	 * <br>The value <code>-1</code> is always ignored.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -416,7 +629,8 @@ public @interface Query {
 	 * A string input is valid if it matches the specified regular expression pattern.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"string"</js>.
@@ -430,7 +644,8 @@ public @interface Query {
 	 * An array or collection is valid if its size is less than, or equal to, the value of this keyword.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"array"</js>.
@@ -444,7 +659,8 @@ public @interface Query {
 	 * An array or collection is valid if its size is greater than, or equal to, the value of this keyword.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"array"</js>.
@@ -458,7 +674,8 @@ public @interface Query {
 	 * If <jk>true</jk> the input validates successfully if all of its elements are unique.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * <br>If the parameter type is a subclass of {@link Set}, this validation is skipped (since a set can only contain unique items anyway).
 	 * <br>Otherwise, the collection or array is checked for duplicate items.
 	 * 
@@ -474,7 +691,8 @@ public @interface Query {
 	 * If specified, the input validates successfully if it is equal to one of the elements in this array.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * The format is a {@link JsonSerializer#DEFAULT_LAX Simple-JSON} array or comma-delimited list.
@@ -508,7 +726,8 @@ public @interface Query {
 	 * <br>The value must be a valid JSON number.
 	 * 
 	 * <p>
-	 * If validation is not met, the method call will throw a {@link BadRequest}.
+	 * If validation is not met during serialization, the part parser will throw a {@link SchemaValidationSerializeException}.
+	 * <br>If validation is not met during parsing, the part parser will throw a {@link SchemaValidationParseException}.
 	 * 
 	 * <p>
 	 * Only allowed for the following types: <js>"integer"</js>, <js>"number"</js>.
@@ -523,7 +742,7 @@ public @interface Query {
 	 * A serialized example of the parameter.
 	 * 
 	 * <p>
-	 * This attribute defines a JSON representation of the value that is used by {@link BasicRestInfoProvider} to construct
+	 * This attribute defines a JSON representation of the value that is used by <code>BasicRestInfoProvider</code> to construct
 	 * an example of the query entry.
 	 * 
 	 * <h5 class='section'>Notes:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Response.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Response.java
similarity index 95%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Response.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Response.java
index aae3f7b..7eb7de8 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Response.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Response.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -18,17 +18,15 @@ import static java.lang.annotation.RetentionPolicy.*;
 import java.lang.annotation.*;
 
 import org.apache.juneau.json.*;
-import org.apache.juneau.rest.exception.*;
-import org.apache.juneau.rest.helper.*;
 
 /**
- * Annotation that can be applied to exceptions and return types that identify the HTTP status they trigger and a description about the exception.
+ * Annotation that can be applied to exceptions and return types on server-side REST methods that identify the HTTP status they trigger and a description about the exception.
  * 
  * <p>
  * When applied to exception classes, this annotation defines Swagger information on non-200 return types.
  * 
  * <p>
- * The following example shows the <ja>@Response</ja> annotation used to define a subclass of {@link Unauthorized} for an invalid login attempt.
+ * The following example shows the <ja>@Response</ja> annotation used to define a subclass of <code>Unauthorized</code> for an invalid login attempt.
  * <br>Note that the annotation can be used on super and subclasses.
  * 
  * <p class='bcode'>
@@ -70,7 +68,7 @@ import org.apache.juneau.rest.helper.*;
  * When applied to return type classes, this annotation defines Swagger information on the body of responses.
  * 
  * <p>
- * In the example above, we're using the {@link Ok} class which is defined like so:
+ * In the example above, we're using the <code>Ok</code> class which is defined like so:
  * 
  * <p class='bcode'>
  * 	<ja>@Response</ja>(code=200, example=<js>"'OK'"</js>)
@@ -78,7 +76,7 @@ import org.apache.juneau.rest.helper.*;
  * </p>
  * 
  * <p>
- * Another example is {@link Redirect} which is defined like so:
+ * Another example is <code>Redirect</code> which is defined like so:
  * 
  * <p class='bcode'>
  * 	<ja>@Response<ja>(
@@ -110,8 +108,10 @@ public @interface Response {
 	 * The HTTP response code.
 	 * 
 	 * The default value is <code>500</code> for exceptions and <code>200</code> for return types.
+	 * 
+	 * TODO - Can also be used on throwable to specify the HTTP status code to set when thrown.
 	 */
-	int code() default 0;
+	int[] code() default {};
 	
 	/**
 	 * A synonym for {@link #code()}.
@@ -130,7 +130,7 @@ public @interface Response {
 	 * 	<jk>public class</jk> NotFound <jk>extends</jk> RestException {...}
 	 * </p>
 	 */
-	int value() default 0;
+	int[] value() default {};
 	
 	/**
 	 * <mk>description</mk> field of the Swagger <a class="doclink" href="https://swagger.io/specification/v2/#responseObject">Response</a> object.
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseHeader.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseHeader.java
similarity index 97%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseHeader.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseHeader.java
index d8e403b..1a8c6e5 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseHeader.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseHeader.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -20,7 +20,6 @@ import java.lang.annotation.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.httppart.oapi.*;
 import org.apache.juneau.json.*;
-import org.apache.juneau.rest.*;
 import org.apache.juneau.utils.*;
 
 /**
@@ -84,20 +83,20 @@ public @interface ResponseHeader {
 	 * 		The default value is <js>"200"</js>.
 	 * </ul>
 	 */
-	int code() default 0;
+	int[] code() default {};
 	
-	/**
-	 * The HTTP status (or statuses) of the response.
-	 * 
-	 * <h5 class='section'>Notes:</h5>
-	 * <ul class='spaced-list'>
-	 * 	<li>
-	 * 		The format is a comma-delimited list of HTTP status codes that this header applies to.
-	 * 	<li>
-	 * 		The default value is <js>"200"</js>.
-	 * </ul>
-	 */
-	int[] codes() default {};
+//	/**
+//	 * The HTTP status (or statuses) of the response.
+//	 * 
+//	 * <h5 class='section'>Notes:</h5>
+//	 * <ul class='spaced-list'>
+//	 * 	<li>
+//	 * 		The format is a comma-delimited list of HTTP status codes that this header applies to.
+//	 * 	<li>
+//	 * 		The default value is <js>"200"</js>.
+//	 * </ul>
+//	 */
+//	int[] codes() default {};
 
 	/**
 	 * The HTTP header name.
@@ -426,7 +425,7 @@ public @interface ResponseHeader {
 	 * TODO
 	 * 
 	 * <p>
-	 * This attribute defines a JSON representation of the body value that is used by {@link BasicRestInfoProvider} to construct
+	 * This attribute defines a JSON representation of the body value that is used by <code>BasicRestInfoProvider</code> to construct
 	 * media-type-based examples of the header value.
 	 * 
 	 * <h5 class='section'>Notes:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseStatus.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseStatus.java
similarity index 98%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseStatus.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseStatus.java
index 5dec704..a8a14a0 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseStatus.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseStatus.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.RetentionPolicy.*;
 
@@ -20,7 +20,7 @@ import org.apache.juneau.json.*;
 import org.apache.juneau.utils.*;
 
 /**
- * Annotation that can be applied to parameters and types to denote them as an HTTP response status.
+ * Annotation that can be applied to parameters and types to denote them as an HTTP response status on server-side REST method parameters.
  * 
  * <p>
  * This can only be applied to parameters and subclasses of the {@link Value} class with an {@link Integer} type.
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseStatuses.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseStatuses.java
similarity index 96%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseStatuses.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseStatuses.java
index 1111aeb..b14a738 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/ResponseStatuses.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/ResponseStatuses.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Responses.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Responses.java
similarity index 96%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Responses.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Responses.java
index 32056c4..ec87e35 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Responses.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Responses.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Schema.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Schema.java
similarity index 96%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Schema.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Schema.java
index f14e2c6..0cffc9b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Schema.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Schema.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
@@ -18,7 +18,6 @@ import static java.lang.annotation.RetentionPolicy.*;
 import java.lang.annotation.*;
 
 import org.apache.juneau.json.*;
-import org.apache.juneau.rest.*;
 
 /**
  * Swagger schema annotation.
@@ -327,7 +326,7 @@ public @interface Schema {
 	 * 		(e.g. <js>"$L{my.localized.variable}"</js>).
 	 * </ul>
 	 */
-	String maxProperties() default "";
+	int maxProperties() default -1;
 	
 	
 	/**
@@ -343,7 +342,7 @@ public @interface Schema {
 	 * 		(e.g. <js>"$L{my.localized.variable}"</js>).
 	 * </ul>
 	 */
-	String minProperties() default "";
+	int minProperties() default -1;
 	
 	/**
 	 * <mk>required</mk> field of the Swagger <a class="doclink" href="https://swagger.io/specification/v2/#schemaObject">Schema</a> object.
@@ -567,7 +566,7 @@ public @interface Schema {
 	 * TODO
 	 * 
 	 * <p>
-	 * This attribute defines a JSON representation of the body value that is used by {@link BasicRestInfoProvider} to construct
+	 * This attribute defines a JSON representation of the body value that is used by <code>BasicRestInfoProvider</code> to construct
 	 * media-type-based examples of the body of the request.
 	 * 
 	 * <h5 class='section'>Notes:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/SubItems.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/SubItems.java
similarity index 97%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/SubItems.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/SubItems.java
index 1fed64c..598526e 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/SubItems.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/SubItems.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Tag.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Tag.java
similarity index 96%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Tag.java
rename to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Tag.java
index 2f4aa58..9bb38b2 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Tag.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Tag.java
@@ -10,7 +10,7 @@
 // * "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.rest.annotation;
+package org.apache.juneau.http.annotation;
 
 import static java.lang.annotation.ElementType.*;
 import static java.lang.annotation.RetentionPolicy.*;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartParser.java
index b3bae5e..0884fcb 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartParser.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartParser.java
@@ -51,7 +51,8 @@ public interface HttpPartParser {
 	 * @param in The input being parsed.
 	 * @param type The category of value being parsed.
 	 * @return The parsed value.
-	 * @throws ParseException
+	 * @throws ParseException If a problem occurred while trying to parse the input.
+	 * @throws SchemaValidationParseException If the input or resulting HTTP part object fails schema validation.
 	 */
-	public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws ParseException;
+	public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws ParseException, SchemaValidationParseException;
 }
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 5682d11..824cae7 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
@@ -15,11 +15,13 @@ package org.apache.juneau.httppart;
 import static java.util.Collections.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
+import java.lang.annotation.*;
 import java.lang.reflect.*;
 import java.util.*;
 import java.util.regex.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.oapi.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.parser.*;
@@ -45,19 +47,25 @@ public class HttpPartSchema {
 	//-------------------------------------------------------------------------------------------------------------------
 
 	/** Reusable instance of {@link OapiPartSerializer}, all default settings. */
-	public static final HttpPartSchema DEFAULT = HttpPartSchema.create().build();
+	public static final HttpPartSchema DEFAULT = HttpPartSchema.create().allowEmptyValue(true).build();
 
+	final String name;
+	final Set<Integer> codes;
 	final String _default;
 	final Set<String> _enum;
 	final Map<String,HttpPartSchema> properties;
... 10549 lines suppressed ...


Mime
View raw message