diff --git a/CHANGES.txt b/CHANGES.txt index b2e91e49..58b581c0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,9 @@ 0.16 - XXXX-XX-XX ================= -- +- Add support for validation of input content other than JSON against Colander + schemas: built-in support of form-urlencoded and configuration hooks for + other content types 0.15 - 2013-10-09 ================= diff --git a/cornice/__init__.py b/cornice/__init__.py index e7ac71b3..63868782 100644 --- a/cornice/__init__.py +++ b/cornice/__init__.py @@ -9,7 +9,8 @@ from cornice.pyramidhook import ( wrap_request, register_service_views, - handle_exceptions + handle_exceptions, + add_deserializer, ) from cornice.util import ContentTypePredicate @@ -38,10 +39,14 @@ def includeme(config): #config.add_directive('add_apidoc', add_apidoc) config.add_directive('add_cornice_service', register_service_views) + config.add_directive('add_cornice_deserializer', add_deserializer) config.add_subscriber(add_renderer_globals, BeforeRender) config.add_subscriber(wrap_request, NewRequest) config.add_renderer('simplejson', util.json_renderer) config.add_view_predicate('content_type', ContentTypePredicate) + config.add_cornice_deserializer('application/x-www-form-urlencoded', + util.extract_form_urlencoded_data) + config.add_cornice_deserializer('application/json', util.extract_json_data) settings = config.get_settings() if settings.get('handle_exceptions', True): diff --git a/cornice/pyramidhook.py b/cornice/pyramidhook.py index cc7e909e..4493f656 100644 --- a/cornice/pyramidhook.py +++ b/cornice/pyramidhook.py @@ -11,8 +11,10 @@ from cornice.service import decorate_view from cornice.errors import Errors -from cornice.util import is_string, to_list, match_accept_header, \ - match_content_type_header, content_type_matches +from cornice.util import ( + is_string, to_list, match_accept_header, match_content_type_header, + content_type_matches, +) from cornice.cors import ( get_cors_validator, get_cors_preflight_view, @@ -174,7 +176,7 @@ def register_service_views(config, service): decorated_view = decorate_view(view, dict(args), method) for item in ('filters', 'validators', 'schema', 'klass', - 'error_handler') + CORS_PARAMETERS: + 'error_handler', 'deserializer') + CORS_PARAMETERS: if item in args: del args[item] @@ -296,3 +298,14 @@ def _mungle_view_args(args, predicate_list): else: # otherwise argument value is just a scalar args[kind] = value + + +def add_deserializer(config, content_type, deserializer): + registry = config.registry + + def callback(): + if not hasattr(registry, 'cornice_deserializers'): + registry.cornice_deserializers = {} + registry.cornice_deserializers[content_type] = deserializer + + config.action(content_type, callable=callback) diff --git a/cornice/service.py b/cornice/service.py index 3780f85d..0ebf1843 100644 --- a/cornice/service.py +++ b/cornice/service.py @@ -478,6 +478,10 @@ def wrapper(request): if is_string(view): view_ = getattr(ob, view.lower()) + # set data deserializer + if 'deserializer' in args: + request.deserializer = args['deserializer'] + # do schema validation if 'schema' in args: validate_colander_schema(args['schema'], request) diff --git a/cornice/tests/test_schemas.py b/cornice/tests/test_schemas.py index 97a82b16..6fdadddf 100644 --- a/cornice/tests/test_schemas.py +++ b/cornice/tests/test_schemas.py @@ -4,6 +4,7 @@ from cornice.errors import Errors from cornice.tests.support import TestCase from cornice.schemas import CorniceSchema, validate_colander_schema +from cornice.util import extract_json_data try: from colander import ( @@ -148,6 +149,11 @@ def __init__(self, body): self.GET = {} self.POST = {} self.validated = {} + self.registry = { + 'cornice_deserializers': { + 'application/json': extract_json_data + } + } dummy_request = MockRequest('{"bar": "required_data"}') setattr(dummy_request, 'errors', Errors(dummy_request)) diff --git a/cornice/tests/test_service_description.py b/cornice/tests/test_service_description.py index 8b09779c..8b54b4ac 100644 --- a/cornice/tests/test_service_description.py +++ b/cornice/tests/test_service_description.py @@ -2,7 +2,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -import json import warnings from pyramid import testing @@ -107,8 +106,8 @@ def test_schema_validation(self): # and if we add the required values in the body of the post, # then we should be good data = {'foo': 'yeah', 'bar': 'open'} - resp = self.app.post('/foobar?yeah=test', - params=json.dumps(data), status=200) + resp = self.app.post_json('/foobar?yeah=test', + params=data, status=200) self.assertEqual(resp.json, {u'baz': None, "test": "succeeded"}) @@ -119,40 +118,41 @@ def test_schema_validation2(self): def test_bar_validator(self): # test validator on bar attribute data = {'foo': 'yeah', 'bar': 'closed'} - resp = self.app.post('/foobar?yeah=test', params=json.dumps(data), - status=400) + resp = self.app.post_json('/foobar?yeah=test', params=data, + status=400) self.assertEqual(resp.json, { u'errors': [{u'description': u'The bar is not open.', - u'location': u'body', - u'name': u'bar'}], + u'location': u'body', + u'name': u'bar'}], u'status': u'error'}) def test_foo_required(self): # test required attribute data = {'bar': 'open'} - resp = self.app.post('/foobar?yeah=test', params=json.dumps(data), - status=400) + resp = self.app.post_json('/foobar?yeah=test', params=data, + status=400) self.assertEqual(resp.json, { u'errors': [{u'description': u'foo is missing', - u'location': u'body', - u'name': u'foo'}], + u'location': u'body', + u'name': u'foo'}], u'status': u'error'}) def test_default_baz_value(self): # test required attribute data = {'foo': 'yeah', 'bar': 'open'} - resp = self.app.post('/foobar?yeah=test', params=json.dumps(data), - status=200) + resp = self.app.post_json('/foobar?yeah=test', params=data, + status=200) self.assertEqual(resp.json, {u'baz': None, "test": "succeeded"}) def test_ipsum_error_message(self): # test required attribute data = {'foo': 'yeah', 'bar': 'open', 'ipsum': 5} - resp = self.app.post('/foobar?yeah=test', params=json.dumps(data), - status=400) + resp = self.app.post_json('/foobar?yeah=test', + params=data, + status=400) self.assertEqual(resp.json, { u'errors': [ @@ -165,8 +165,8 @@ def test_integers_fail(self): # test required attribute data = {'foo': 'yeah', 'bar': 'open', 'ipsum': 2, 'integers': ('a', '2')} - resp = self.app.post('/foobar?yeah=test', params=json.dumps(data), - status=400) + resp = self.app.post_json('/foobar?yeah=test', data, + status=400) self.assertEqual(resp.json, { u'errors': [ @@ -179,8 +179,8 @@ def test_integers_ok(self): # test required attribute data = {'foo': 'yeah', 'bar': 'open', 'ipsum': 2, 'integers': ('1', '2')} - self.app.post('/foobar?yeah=test', params=json.dumps(data), - status=200) + self.app.post_json('/foobar?yeah=test', params=data, + status=200) def test_nested_schemas(self): @@ -190,9 +190,9 @@ def test_nested_schemas(self): nested_data = {"title": "Mushroom", "fields": [{"schmil": "Blick"}]} - self.app.post('/nested', params=json.dumps(data), status=200) - self.app.post('/nested', params=json.dumps(nested_data), - status=400) + self.app.post_json('/nested', params=data, status=200) + self.app.post_json('/nested', params=nested_data, + status=400) def test_qux_header(self): resp = self.app.delete('/foobar', status=400) @@ -204,4 +204,4 @@ def test_qux_header(self): u'status': u'error'}) self.app.delete('/foobar', headers={'X-Qux': 'Hotzenplotz'}, - status=200) + status=200) diff --git a/cornice/tests/test_validation.py b/cornice/tests/test_validation.py index 5b53c085..7822d719 100644 --- a/cornice/tests/test_validation.py +++ b/cornice/tests/test_validation.py @@ -1,14 +1,15 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +from pyramid.config import Configurator import simplejson as json from webtest import TestApp from pyramid.response import Response from cornice.errors import Errors -from cornice.tests.validationapp import main -from cornice.tests.support import LoggingCatcher, TestCase +from cornice.tests.validationapp import main, includeme, dummy_deserializer +from cornice.tests.support import LoggingCatcher, TestCase, CatchErrors from cornice.validators import filter_json_xsrf @@ -18,44 +19,45 @@ def test_validation(self): app = TestApp(main({})) app.get('/service', status=400) - res = app.post('/service', params='buh', status=400) - self.assertTrue(b'Not a json body' in res.body) + response = app.post('/service', params='buh', status=400) + self.assertTrue(b'Not a json body' in response.body) - res = app.post('/service', params=json.dumps('buh')) + response = app.post('/service', params=json.dumps('buh')) expected = json.dumps({'body': '"buh"'}).encode('ascii') - self.assertEqual(res.body, expected) + self.assertEqual(response.body, expected) app.get('/service?paid=yup') # valid = foo is one - res = app.get('/service?foo=1&paid=yup') - self.assertEqual(res.json['foo'], 1) + response = app.get('/service?foo=1&paid=yup') + self.assertEqual(response.json['foo'], 1) # invalid value for foo - res = app.get('/service?foo=buh&paid=yup', status=400) + response = app.get('/service?foo=buh&paid=yup', status=400) # check that json is returned - errors = Errors.from_json(res.body) + errors = Errors.from_json(response.body) self.assertEqual(len(errors), 1) def test_validation_hooked_error_response(self): app = TestApp(main({})) - res = app.post('/service4', status=400) - self.assertTrue(b'' in res.body) + response = app.post('/service4', status=400) + self.assertTrue(b'' in response.body) def test_accept(self): # tests that the accept headers are handled the proper way app = TestApp(main({})) # requesting the wrong accept header should return a 406 ... - res = app.get('/service2', headers={'Accept': 'audio/*'}, status=406) + response = app.get('/service2', headers={'Accept': 'audio/*'}, + status=406) # ... with the list of accepted content-types - error_location = res.json['errors'][0]['location'] - error_name = res.json['errors'][0]['name'] - error_description = res.json['errors'][0]['description'] + error_location = response.json['errors'][0]['location'] + error_name = response.json['errors'][0]['name'] + error_description = response.json['errors'][0]['description'] self.assertEquals('header', error_location) self.assertEquals('Accept', error_name) self.assertTrue('application/json' in error_description) @@ -63,55 +65,59 @@ def test_accept(self): self.assertTrue('text/plain' in error_description) # requesting a supported type should give an appropriate response type - r = app.get('/service2', headers={'Accept': 'application/*'}) - self.assertEqual(r.content_type, "application/json") + response = app.get('/service2', headers={'Accept': 'application/*'}) + self.assertEqual(response.content_type, "application/json") - r = app.get('/service2', headers={'Accept': 'text/plain'}) - self.assertEqual(r.content_type, "text/plain") + response = app.get('/service2', headers={'Accept': 'text/plain'}) + self.assertEqual(response.content_type, "text/plain") # it should also work with multiple Accept headers - r = app.get('/service2', headers={'Accept': 'audio/*, application/*'}) - self.assertEqual(r.content_type, "application/json") + response = app.get('/service2', headers={ + 'Accept': 'audio/*, application/*' + }) + self.assertEqual(response.content_type, "application/json") # and requested preference order should be respected headers = {'Accept': 'application/json; q=1.0, text/plain; q=0.9'} - r = app.get('/service2', headers=headers) - self.assertEqual(r.content_type, "application/json") + response = app.get('/service2', headers=headers) + self.assertEqual(response.content_type, "application/json") headers = {'Accept': 'application/json; q=0.9, text/plain; q=1.0'} - r = app.get('/service2', headers=headers) - self.assertEqual(r.content_type, "text/plain") + response = app.get('/service2', headers=headers) + self.assertEqual(response.content_type, "text/plain") # test that using a callable to define what's accepted works as well - res = app.get('/service3', headers={'Accept': 'audio/*'}, status=406) - error_description = res.json['errors'][0]['description'] + response = app.get('/service3', headers={'Accept': 'audio/*'}, + status=406) + error_description = response.json['errors'][0]['description'] self.assertTrue('text/json' in error_description) - res = app.get('/service3', headers={'Accept': 'text/*'}) - self.assertEqual(res.content_type, "text/json") + response = app.get('/service3', headers={'Accept': 'text/*'}) + self.assertEqual(response.content_type, "text/json") # if we are not asking for a particular content-type, # we should get one of the two types that the service supports. - r = app.get('/service2') - self.assertTrue(r.content_type in ("application/json", "text/plain")) + response = app.get('/service2') + self.assertTrue(response.content_type + in ("application/json", "text/plain")) def test_accept_issue_113_text_star(self): app = TestApp(main({})) - res = app.get('/service3', headers={'Accept': 'text/*'}) - self.assertEqual(res.content_type, "text/json") + response = app.get('/service3', headers={'Accept': 'text/*'}) + self.assertEqual(response.content_type, "text/json") def test_accept_issue_113_text_application_star(self): app = TestApp(main({})) - res = app.get('/service3', headers={'Accept': 'application/*'}) - self.assertEqual(res.content_type, "application/json") + response = app.get('/service3', headers={'Accept': 'application/*'}) + self.assertEqual(response.content_type, "application/json") def test_accept_issue_113_text_application_json(self): app = TestApp(main({})) - res = app.get('/service3', headers={'Accept': 'application/json'}) - self.assertEqual(res.content_type, "application/json") + response = app.get('/service3', headers={'Accept': 'application/json'}) + self.assertEqual(response.content_type, "application/json") def test_accept_issue_113_text_html_not_acceptable(self): app = TestApp(main({})) @@ -123,14 +129,16 @@ def test_accept_issue_113_text_html_not_acceptable(self): def test_accept_issue_113_audio_or_text(self): app = TestApp(main({})) - res = app.get('/service2', - headers={'Accept': 'audio/mp4; q=0.9, text/plain; q=0.5'}) - self.assertEqual(res.content_type, "text/plain") + response = app.get('/service2', headers={ + 'Accept': 'audio/mp4; q=0.9, text/plain; q=0.5' + }) + self.assertEqual(response.content_type, "text/plain") # if we are not asking for a particular content-type, # we should get one of the two types that the service supports. - r = app.get('/service2') - self.assertTrue(r.content_type in ("application/json", "text/plain")) + response = app.get('/service2') + self.assertTrue(response.content_type + in ("application/json", "text/plain")) def test_filters(self): app = TestApp(main({})) @@ -139,37 +147,33 @@ def test_filters(self): self.assertTrue(b"filtered response" in app.get('/filtered').body) self.assertTrue(b"unfiltered" in app.post('/filtered').body) - def test_json_xsrf(self): - - def json_response(string_value): - resp = Response(string_value) - resp.status = 200 - resp.content_type = 'application/json' - filter_json_xsrf(resp) - - # a view returning a vulnerable json response should issue a warning - for value in [ + def test_json_xsrf_vulnerable_values_warning(self): + vulnerable_values = [ '["value1", "value2"]', # json array ' \n ["value1", "value2"] ', # may include whitespace '"value"', # strings may contain nasty characters in UTF-7 - ]: - resp = Response(value) - resp.status = 200 - resp.content_type = 'application/json' - filter_json_xsrf(resp) + ] + # a view returning a vulnerable json response should issue a warning + for value in vulnerable_values: + response = Response(value) + response.status = 200 + response.content_type = 'application/json' + filter_json_xsrf(response) assert len(self.get_logs()) == 1, "Expected warning: %s" % value - # a view returning safe json response should not issue a warning - for value in [ + def test_json_xsrf_safe_values_no_warning(self): + safe_values = [ '{"value1": "value2"}', # json object ' \n {"value1": "value2"} ', # may include whitespace 'true', 'false', 'null', # primitives '123', '-123', '0.123', # numbers - ]: - resp = Response(value) - resp.status = 200 - resp.content_type = 'application/json' - filter_json_xsrf(resp) + ] + # a view returning safe json response should not issue a warning + for value in safe_values: + response = Response(value) + response.status = 200 + response.content_type = 'application/json' + filter_json_xsrf(response) assert len(self.get_logs()) == 0, "Unexpected warning: %s" % value def test_multiple_querystrings(self): @@ -183,8 +187,8 @@ def test_multiple_querystrings(self): def test_email_field(self): app = TestApp(main({})) - content = json.dumps({'email': 'alexis@notmyidea.org'}) - app.post('/newsletter', params=content) + content = {'email': 'alexis@notmyidea.org'} + app.post_json('/newsletter', params=content) def test_content_type_missing(self): # test that a Content-Type request headers is present @@ -208,7 +212,8 @@ def test_content_type_wrong_single(self): # requesting the wrong Content-Type header should return a 415 ... response = app.post('/service5', - headers={'Content-Type': 'text/plain'}, status=415) + headers={'Content-Type': 'text/plain'}, + status=415) # ... with an appropriate json error structure error_description = response.json['errors'][0]['description'] @@ -220,7 +225,8 @@ def test_content_type_wrong_multiple(self): # requesting the wrong Content-Type header should return a 415 ... response = app.put('/service5', - headers={'Content-Type': 'text/xml'}, status=415) + headers={'Content-Type': 'text/xml'}, + status=415) # ... with an appropriate json error structure error_description = response.json['errors'][0]['description'] @@ -233,8 +239,9 @@ def test_content_type_correct(self): # requesting with one of the allowed Content-Type headers should work, # even when having a charset parameter as suffix - response = app.put('/service5', - headers={'Content-Type': 'text/plain; charset=utf-8'}) + response = app.put('/service5', headers={ + 'Content-Type': 'text/plain; charset=utf-8' + }) self.assertEqual(response.json, "some response") def test_content_type_on_get(self): @@ -247,9 +254,9 @@ def test_content_type_on_get(self): def test_content_type_with_callable(self): # test that using a callable for content_type works as well app = TestApp(main({})) - res = app.post('/service6', headers={'Content-Type': 'audio/*'}, - status=415) - error_description = res.json['errors'][0]['description'] + response = app.post('/service6', headers={'Content-Type': 'audio/*'}, + status=415) + error_description = response.json['errors'][0]['description'] self.assertTrue('text/xml' in error_description) self.assertTrue('application/json' in error_description) @@ -261,37 +268,97 @@ def test_accept_and_content_type(self): app = TestApp(main({})) # POST endpoint just has one accept and content_type definition - response = app.post('/service7', - headers={ - 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/json; charset=utf-8'}) + response = app.post('/service7', headers={ + 'Accept': 'text/xml, application/json', + 'Content-Type': 'application/json; charset=utf-8' + }) self.assertEqual(response.json, "some response") - response = app.post('/service7', + response = app.post( + '/service7', headers={ 'Accept': 'text/plain, application/json', - 'Content-Type': 'application/json; charset=utf-8'}, status=406) + 'Content-Type': 'application/json; charset=utf-8' + }, + status=406) - response = app.post('/service7', + response = app.post( + '/service7', headers={ 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/x-www-form-urlencoded'}, + 'Content-Type': 'application/x-www-form-urlencoded' + }, status=415) # PUT endpoint has a list of accept and content_type definitions - response = app.put('/service7', - headers={ - 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/json; charset=utf-8'}) + response = app.put('/service7', headers={ + 'Accept': 'text/xml, application/json', + 'Content-Type': 'application/json; charset=utf-8' + }) self.assertEqual(response.json, "some response") - response = app.put('/service7', + response = app.put( + '/service7', headers={ 'Accept': 'audio/*', - 'Content-Type': 'application/json; charset=utf-8'}, status=406) + 'Content-Type': 'application/json; charset=utf-8' + }, + status=406) - response = app.put('/service7', + response = app.put( + '/service7', headers={ 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/x-www-form-urlencoded'}, - status=415) + 'Content-Type': 'application/x-www-form-urlencoded' + }, status=415) + + +class TestRequestDataExtractors(LoggingCatcher, TestCase): + + def make_ordinary_app(self): + return TestApp(main({})) + + def make_app_with_deserializer(self, deserializer): + config = Configurator(settings={}) + config.include(includeme) + config.add_cornice_deserializer('text/dummy', deserializer) + return TestApp(CatchErrors(config.make_wsgi_app())) + + def test_json(self): + app = self.make_ordinary_app() + response = app.post_json('/foobar?yeah=test', { + 'foo': 'hello', + 'bar': 'open', + 'yeah': 'man', + }) + self.assertEqual(response.json['test'], 'succeeded') + + def test_www_form_urlencoded(self): + app = self.make_ordinary_app() + response = app.post('/foobar?yeah=test', { + 'foo': 'hello', + 'bar': 'open', + 'yeah': 'man', + }) + self.assertEqual(response.json['test'], 'succeeded') + + def test_deserializer_from_global_config(self): + app = self.make_app_with_deserializer(dummy_deserializer) + response = app.post('/foobar?yeah=test', "hello,open,yeah", + headers={'content-type': 'text/dummy'}) + self.assertEqual(response.json['test'], 'succeeded') + + def test_deserializer_from_view_config(self): + app = self.make_ordinary_app() + response = app.post('/custom_deserializer?yeah=test', + "hello,open,yeah", + headers={'content-type': 'text/dummy'}) + self.assertEqual(response.json['test'], 'succeeded') + + def test_view_config_has_priority_over_global_config(self): + low_priority_deserializer = lambda request: "we don't want this" + app = self.make_app_with_deserializer(low_priority_deserializer) + response = app.post('/custom_deserializer?yeah=test', + "hello,open,yeah", + headers={'content-type': 'text/dummy'}) + self.assertEqual(response.json['test'], 'succeeded') diff --git a/cornice/tests/validationapp.py b/cornice/tests/validationapp.py index 823327dd..ef08dad7 100644 --- a/cornice/tests/validationapp.py +++ b/cornice/tests/validationapp.py @@ -180,6 +180,18 @@ class FooBarSchema(MappingSchema): def foobar_post(request): return {"test": "succeeded"} + custom_deserializer_service = Service(name="custom_deserializer_service", + path="/custom_deserializer") + + def dummy_deserializer(request): + values = request.body.decode().split(',') + return dict(zip(['foo', 'bar', 'yeah'], values)) + + @custom_deserializer_service.post(schema=FooBarSchema, + deserializer=dummy_deserializer) + def custom_deserializer_service_post(request): + return {"test": "succeeded"} + class StringSequence(SequenceSchema): _ = SchemaNode(String()) diff --git a/cornice/util.py b/cornice/util.py index d448414c..529f8e87 100644 --- a/cornice/util.py +++ b/cornice/util.py @@ -84,20 +84,38 @@ def match_content_type_header(func, context, request): return content_type_matches(request, supported_contenttypes) -def extract_request_data(request): - """extract the different parts of the data from the request, and return - them as a tuple of (querystring, headers, body, path) - """ - # XXX In the body, we're only handling JSON for now. +def extract_json_data(request): if request.body: try: body = json.loads(request.body) except ValueError as e: - request.errors.add('body', None, - "Invalid JSON request body: %s" % (e.message)) - body = {} + request.errors.add( + 'body', None, + "Invalid JSON request body: %s" % (e.message)) + return body else: - body = {} + return {} + + +def extract_form_urlencoded_data(request): + return request.POST + + +def extract_request_data(request): + """extract the different parts of the data from the request, and return + them as a tuple of (querystring, headers, body, path) + """ + body = {} + content_type = getattr(request, 'content_type', None) + registry = request.registry + if hasattr(request, 'deserializer'): + body = request.deserializer(request) + elif (hasattr(registry, 'cornice_deserializers') + and content_type in registry.cornice_deserializers): + deserializer = registry.cornice_deserializers[content_type] + body = deserializer(request) + # otherwise, don't block but it will be an empty body, decode + # on your own return request.GET, request.headers, body, request.matchdict diff --git a/docs/source/validation.rst b/docs/source/validation.rst index 74920ac7..2e87126b 100644 --- a/docs/source/validation.rst +++ b/docs/source/validation.rst @@ -136,6 +136,40 @@ If you want the schema to be dynamic, i.e. you want to chose which one to use pe schema = schema().bind(context=self.context, request=self.request) return CorniceSchema(schema.children) +Cornice provides built-in support for JSON and HTML forms +(``application/x-www-form-urlencoded``) input validation using Colander. If +you need to validate other input formats, such as XML, you can provide callable +objects taking a ``request`` argument and returning a Python data structure +that Colander can understand:: + + def dummy_deserializer(request): + return parse_my_input_format(request.body) + + +You can then instruct a specific view to use it with the ``deserializer`` +parameter:: + + @foobar.post(schema=FooBarSchema, deserializer=dummy_deserializer) + def foobar_post(request): + return {"test": "succeeded"} + + +If you'd like to configure deserialization globally, you can use the +``add_cornice_deserializer`` configuration directive in your app configuration +code to tell Cornice which deserializer to use for a given content +type:: + + config = Configurator(settings={}) + # ... + config.add_cornice_deserializer('text/dummy', dummy_deserializer) + +With this configuration, when a request comes with a Content-Type header set to +``text/dummy``, Cornice will call ``dummy_deserializer`` on the ``request`` +before passing the result to Colander. + +View-specific deserializers have priority over global content-type +deserializers. + Using formencode ~~~~~~~~~~~~~~~~ @@ -249,20 +283,6 @@ There are two flavors of content validations cornice can apply to services: Otherwise it will croak with a ``406 Not Acceptable``. -Content validation -================== - -There are two flavors of content validations cornice can apply to services: - - - **Content-Type validation** will match the ``Content-Type`` header sent - by the client against a list of allowed content types. - When failing on that, it will croak with a ``415 Unsupported Media Type``. - - - **Content negotiation** checks if cornice is able to respond with the - requested content type asked by the client sending an ``Accept`` header. - Otherwise it will croak with a ``406 Not Acceptable``. - - Content-Type validation -----------------------