ЁЯПл The SchoolтА║ЁЯЫОя╕П API GatewayтА║ЁЯУЭ рдзрдбрд╛ 04 тАФ Validation рдЖрдгрд┐ mapping: form рдЪреА рддрдкрд╛рд╕рдгреА рдЖрдгрд┐ рд╕реНрд╡рдЪреНрдЫ рдЙрддреНрддрд░
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУЭ рдзрдбрд╛ 04 тАФ Validation рдЖрдгрд┐ mapping: form рдЪреА рддрдкрд╛рд╕рдгреА рдЖрдгрд┐ рд╕реНрд╡рдЪреНрдЫ рдЙрддреНрддрд░

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 04 ┬╖ рдорд╛рдЧреЗ: lesson-03-routes-integrations ┬╖ рдкреБрдвреЗ: lesson-05-stages-deployments


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ03, рдЖрдгрд┐ office рдЖрдд рдпреЗрддрд╛рдирд╛ рдЖрдгрд┐ рдмрд╛рд╣реЗрд░ рдЬрд╛рддрд╛рдирд╛ рдХрд░рддреЗ рддреА рджреЛрди рдХрд╛рдореЗ: request validation (kitchen рд▓рд╛ call рдХрд░рдгреНрдпрд╛рдЖрдзреАрдЪ рдЪреБрдХреАрдЪреА body 400 рдиреЗ рдирд╛рдХрд╛рд░рд▓реА рдЬрд╛рддреЗ) рдЖрдгрд┐ response mapping (office kitchen рдЪреНрдпрд╛ рдЙрддреНрддрд░рд╛рддреВрди phone number рдХрд╛рдвреВрди рдЯрд╛рдХрддреЗ). apigw/demo.py рдордзрд▓реЗ validate() рджреЛрдиреНрд╣реА рджрд╛рдЦрд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдПрдХ рд╢рд┐рдХреНрд╖рд┐рдХрд╛ clerk рдХрдбреЗ рдПрдХ marks form рджреЗрддреЗ. Form рдордзрд▓реЗ рддреАрди рд░рдХрд╛рдиреЗ рднрд░рд▓реЗрд▓реЗ рдЕрд╕рд╛рдпрд▓рд╛рдЪ рд╣рд╡реЗрдд: рд╡рд┐рджреНрдпрд╛рд░реНрдереА, рд╡рд┐рд╖рдп, mark. рдЖрдгрд┐ mark рд╣рд╛ рдЖрдХрдбрд╛ рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡рд╛.

Clerk form desk рд╡рд░рдЪ тЬНя╕П рддрдкрд╛рд╕рддреЛ. рдПрдЦрд╛рджрд╛ рд░рдХрд╛рдирд╛ рд░рд┐рдХрд╛рдорд╛ рдЖрд╣реЗ? "рдХреГрдкрдпрд╛ mark рднрд░рд╛." Mark рдЕрдХреНрд╖рд░рд╛рдВрдд "рдирд╡реНрд╡рдж" рдЕрд╕рд╛ рд▓рд┐рд╣рд┐рд▓рд╛ рдЖрд╣реЗ? "Mark рдЖрдХрдбреНрдпрд╛рддрдЪ рд╣рд╡рд╛." Form рд╢рд┐рдХреНрд╖рд┐рдХреЗрдХрдбреЗ рдкрд░рдд рдЬрд╛рддреЛ тАФ kitchen рд▓рд╛ рддреЛ рдХрдзреА рджрд┐рд╕рддрдЪ рдирд╛рд╣реА, рдЖрдгрд┐ рддреНрдпрд╛рд╡рд░ kitchen рдЪрд╛ рд╡реЗрд│рд╣реА рд╡рд╛рдпрд╛ рдЬрд╛рдд рдирд╛рд╣реА.

рдмрд╛рд╣реЗрд░ рдЬрд╛рддрд╛рдирд╛, kitchen рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдЪреЗ card рдкрд░рдд рдкрд╛рдард╡рддреЗ рдЬреНрдпрд╛рд╡рд░ рдЕрдЬреВрдирд╣реА phone number рдЖрд╣реЗ. Card рджреЗрдгреНрдпрд╛рдЖрдзреА clerk рддреЛ phone number рдЭрд╛рдХрддреЛ. Kitchen рд╕реЛрдкреЗ рд░рд╛рд╣рддреЗ; visitor рд▓рд╛ рдлрдХреНрдд рддреНрдпрд╛рдиреЗ рдкрд╛рд╣рд╛рдпрд▓рд╛ рд╣рд╡реЗ рддреЗрд╡рдвреЗрдЪ рджрд┐рд╕рддреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    t["ЁЯСйтАНЁЯПл POST /marks"] --> chk{"ЁЯУЭ form check<br/>required: student, subject, mark<br/>mark must be int"}
    chk -->|"mark missing"| e1["тЭМ 400 missing required field 'mark'"]
    chk -->|"mark = 'ninety'"| e2["тЭМ 400 'mark' must be int"]
    chk -->|"ok"| k["ЁЯН│ kitchen saves it тЖТ 201"]
    k2["ЁЯН│ GET /students/7<br/>id ┬╖ name ┬╖ class ┬╖ phone"] --> map["тЬВя╕П response mapping<br/>remove phone"] --> out["ЁЯУД id ┬╖ name ┬╖ class"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/apigateway/lesson-diagrams.html#l04

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг kitchen рдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪрд▓реЗрд▓реНрдпрд╛ рдЪреБрдХреАрдЪреНрдпрд╛ form рдЪреА рдХрд┐рдВрдордд рдореНрд╣рдгрдЬреЗ рдПрдХ Lambda run, рдХрджрд╛рдЪрд┐рдд рдПрдХ database call, рдЖрдгрд┐ caller рд╕рд╛рдареА рдЧреЛрдВрдзрд│рд╛рдд рдЯрд╛рдХрдгрд╛рд░рд╛ 500. рджрд╛рд░рд╛рд╡рд░рдЪ рдЖрдХрд╛рд░ рддрдкрд╛рд╕рд▓рд╛ рдХреА рд╕реНрдкрд╖реНрдЯ 400 рдорд┐рд│рддреЛ, back end рдХрдЪрд▒реНрдпрд╛рдкрд╛рд╕реВрди рд╕реБрд░рдХреНрд╖рд┐рдд рд░рд╛рд╣рддреЛ, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ client рд╕рд╛рдареА рдПрдХрдЪ рд▓рд┐рдЦрд┐рдд рдХрд░рд╛рд░ (model) рд░рд╛рд╣рддреЛ. Response mapping рдореБрд│реЗ рдЦрд╛рдЬрдЧреА fields тАФ phone numbers, рдЖрддрд▓реЗ ids тАФ рдлрдХреНрдд back end рдиреЗ рдкрд░рдд рджрд┐рд▓реЗ рдореНрд╣рдгреВрди рдмрд╛рд╣реЗрд░ рдЬрд╛рдд рдирд╛рд╣реАрдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

apigw/gateway.py рдордзрд▓реЗ validate(schema, body) рд╣реЗ рдПрдХ рдЫреЛрдЯреЗрд╕реЗ JSON-schema рдЖрд╣реЗ: required fields рдЖрдгрд┐ types. office() рдордзрд▓реНрдпрд╛ POST /marks route рд▓рд╛ schema={"required": ["student", "subject", "mark"], "types": {"mark": int}} рдЖрд╣реЗ. Gateway._handle() рдордзреНрдпреЗ validation integration рдЪреНрдпрд╛ рдЖрдзреА рдЪрд╛рд▓рддреЗ, рддреНрдпрд╛рдореБрд│реЗ рдЪреБрдХреАрдЪреА body рдХрдзреАрдЪ add_mark() рдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪрдд рдирд╛рд╣реА. GET /students/{id} route рд▓рд╛ mapping=hide_phone рдЖрд╣реЗ, рдЬреЗ kitchen рдЪреНрдпрд╛ рдЙрддреНрддрд░рд╛рд╡рд░ рдмрд╛рд╣реЗрд░ рдЬрд╛рддрд╛рдирд╛ рд▓рд╛рдЧреВ рд╣реЛрддреЗ тАФ custom integration рд╡рд░рдЪреНрдпрд╛ response mapping template рдЪреА model рдордзрд▓реА рдЖрд╡реГрддреНрддреА. Students kitchen рд▓рд╛ рдЦрд░реЛрдЦрд░ рдХрд┐рддреА рд╡реЗрд│рд╛ call рдХреЗрд▓реЗ рддреЗ calls["n"] рдореЛрдЬрддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 apigw/demo.py validate
python3 - <<'EOF'
import sys; sys.path.insert(0, "apigw"); from demo import office, token, calls
from gateway import validate
gw, c = office()
H = {"Authorization": "Bearer " + token(c, scope="marks:write")}
for body in (["7", "maths", 91], {"student": "7", "subject": "maths", "mark": 91.5}, None):
    print(gw.handle("POST", "/marks", H, body)[0::2])
schema = {"required": ["student", "subject", "mark"], "types": {"mark": int}}
print(validate(schema, {"student": "7", "subject": "maths"}))
calls["n"] = 0; gw.handle("GET", "/students/9"); print("kitchen calls:", calls["n"], "┬╖ answer:", gw.handle("GET", "/students/9")[2])
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

validate рджреЛрди рдирдХрд╛рд░ рдЖрдгрд┐ рдПрдХ рдпрд╢ print рдХрд░рддреЗ: POST /marks тЖТ 400 {'message': 'Invalid request body', 'why': "missing required field 'mark'"}, POST /marks тЖТ 400 {'message': 'Invalid request body', 'why': "'mark' must be int"}, POST /marks тЖТ 201 {'saved': {'student': '7', 'subject': 'maths', 'mark': 91}, 'table': 'marks-prod'}. рдордЧ mapping: kitchen: {'id': '7', 'name': 'Aishwarya', 'class': '8B', 'phone': '98xxxxxx12'} рдЖрдгрд┐ GET /students/7 тЖТ 200 {'id': '7', 'name': 'Aishwarya', 'class': '8B'} тАФ phone рдирд╛рд╣реА.

рддреБрдордЪрд╛ snippet list рд╕рд╛рдареА (400, {'message': 'Invalid request body', 'why': 'body must be a JSON object'}), 91.5 рд╕рд╛рдареА (400, {'message': 'Invalid request body', 'why': "'mark' must be int"}), body рдирд╕рддрд╛рдирд╛ рдкреБрдиреНрд╣рд╛ body must be a JSON object, рдордЧ missing required field 'mark', рдЖрдгрд┐ kitchen calls: 1 ┬╖ answer: {'id': '9', 'name': 'Katrina', 'class': '9A'} print рдХрд░рддреЛ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдЪреБрдХреАрдЪрд╛ form рдХрд╛рд░рдгрд╛рд╕рд╣ desk рд╡рд░рдЪ рдирд╛рдХрд╛рд░рд▓рд╛ рдЬрд╛рддреЛ, рддреНрдпрд╛рд╕рд╛рдареА kitchen рд▓рд╛ call рд╣реЛрдд рдирд╛рд╣реА, рдЖрдгрд┐ kitchen рди рдмрджрд▓рддрд╛ рдмрд╛рд╣реЗрд░ рдЬрд╛рддрд╛рдирд╛ рдЙрддреНрддрд░ рд╕реНрд╡рдЪреНрдЫ рдХрд░рддрд╛ рдпреЗрддреЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

On a real account тАФ REST API рд╕рд╛рдареА OpenAPI рдордзрд▓реЗ model рдЖрдгрд┐ validator:

x-amazon-apigateway-request-validators:
  body-only: { validateRequestBody: true, validateRequestParameters: false }
paths:
  /marks:
    post:
      x-amazon-apigateway-request-validator: body-only
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [student, subject, mark]
              properties:
                student: { type: string }
                subject: { type: string }
                mark:    { type: integer, minimum: 0, maximum: 100 }

Phone рдХрд╛рдвреВрди рдЯрд╛рдХрдгрд╛рд░реЗ, custom Lambda integration рд╡рд░рдЪреЗ response mapping template (VTL):

#set($s = $input.path('$'))
{ "id": "$s.id", "name": "$s.name", "class": "$s.class" }

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: request validation partners рдирд╛ рд╕реНрдкрд╖реНрдЯ 400 рджреЗрддреЗ рдЖрдгрд┐ database рдкрд╛рд╕реВрди рдХрдЪрд░рд╛ рджреВрд░ рдареЗрд╡рддреЗ; fields рдмрд╛рд╣реЗрд░ рдЬрд╛рдгреНрдпрд╛рд╡рд┐рд░реБрджреНрдз response mapping рд╣реА рд╢реЗрд╡рдЯрдЪреА рдлрд│реА рдЖрд╣реЗ. рджреЛрдиреНрд╣реА aws apigateway test-invoke-method рдиреЗ test рдХрд░рд╛.

тПня╕П рдкреБрдвреЗ

Desk рдЪрд╛рд▓рддреЛ. рдЦрд░рд╛ рд╢рд╛рд│реЗрдЪрд╛ рджрд┐рд╡рд╕ рди рдмрд┐рдШрдбрд╡рддрд╛ рддреЛ рдХрд╕рд╛ рдмрджрд▓рд╛рдпрдЪрд╛? Stages рдЖрдгрд┐ deployments тАФ рд░рдВрдЧреАрдд рддрд╛рд▓реАрдо рдЖрдгрд┐ рдЦрд░рд╛ рдкреНрд░рдпреЛрдЧ.

git checkout lesson-05-stages-deployments

ЁЯУЭ Lesson 04 тАФ Validation & mapping: the form check and the clean answer

ЁЯУН You are here: Lesson 04 of 12 ┬╖ Previous: lesson-03-routes-integrations ┬╖ Next: lesson-05-stages-deployments


ЁЯУж What's in this branch

Lessons 01тАУ03, plus two jobs the office does on the way in and the way out: request validation (a bad body is refused with 400 before the kitchen is called) and response mapping (the office removes the phone number from the kitchen's answer). validate() in apigw/demo.py shows both.

ЁЯзТ Explain like I'm 5

A teacher hands a marks form to the clerk. The form must have three boxes filled: student, subject, mark. And the mark must be a number.

The clerk checks the form at the desk тЬНя╕П. A box is empty? "Please fill in the mark." The mark says "ninety" in words? "The mark must be a number." The form goes back to the teacher тАФ the kitchen never sees it, and never wastes time on it.

On the way out, the kitchen sends back a student card that still has the phone number on it. The clerk covers the phone number before handing the card over. The kitchen stays simple; the visitor sees only what they should.

ЁЯЧ║я╕П Diagram

flowchart LR
    t["ЁЯСйтАНЁЯПл POST /marks"] --> chk{"ЁЯУЭ form check<br/>required: student, subject, mark<br/>mark must be int"}
    chk -->|"mark missing"| e1["тЭМ 400 missing required field 'mark'"]
    chk -->|"mark = 'ninety'"| e2["тЭМ 400 'mark' must be int"]
    chk -->|"ok"| k["ЁЯН│ kitchen saves it тЖТ 201"]
    k2["ЁЯН│ GET /students/7<br/>id ┬╖ name ┬╖ class ┬╖ phone"] --> map["тЬВя╕П response mapping<br/>remove phone"] --> out["ЁЯУД id ┬╖ name ┬╖ class"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/apigateway/lesson-diagrams.html#l04

тЭУ What

ЁЯдФ Why

Because a bad form that reaches the kitchen costs a Lambda run, maybe a database call, and a confusing 500 for the caller. Checking the shape at the door gives a clear 400, protects the back end from junk, and keeps one written contract (the model) for every client. Response mapping keeps private fields тАФ phone numbers, internal ids тАФ from leaking just because the back end returned them.

ЁЯФз How (in this repo)

validate(schema, body) in apigw/gateway.py is a tiny JSON-schema: required fields and types. The route POST /marks in office() has schema={"required": ["student", "subject", "mark"], "types": {"mark": int}}. In Gateway._handle() validation runs before the integration, so a bad body never reaches add_mark(). The route GET /students/{id} has mapping=hide_phone, applied to the kitchen's answer on the way out тАФ the model's version of a response mapping template on a custom integration. calls["n"] counts how often the students kitchen was really called.

ЁЯзк Try it

python3 apigw/demo.py validate
python3 - <<'EOF'
import sys; sys.path.insert(0, "apigw"); from demo import office, token, calls
from gateway import validate
gw, c = office()
H = {"Authorization": "Bearer " + token(c, scope="marks:write")}
for body in (["7", "maths", 91], {"student": "7", "subject": "maths", "mark": 91.5}, None):
    print(gw.handle("POST", "/marks", H, body)[0::2])
schema = {"required": ["student", "subject", "mark"], "types": {"mark": int}}
print(validate(schema, {"student": "7", "subject": "maths"}))
calls["n"] = 0; gw.handle("GET", "/students/9"); print("kitchen calls:", calls["n"], "┬╖ answer:", gw.handle("GET", "/students/9")[2])
EOF

тЬЕ Verify тАФ what you should see

validate prints two refusals and one success: POST /marks тЖТ 400 {'message': 'Invalid request body', 'why': "missing required field 'mark'"}, POST /marks тЖТ 400 {'message': 'Invalid request body', 'why': "'mark' must be int"}, POST /marks тЖТ 201 {'saved': {'student': '7', 'subject': 'maths', 'mark': 91}, 'table': 'marks-prod'}. Then the mapping: kitchen: {'id': '7', 'name': 'Aishwarya', 'class': '8B', 'phone': '98xxxxxx12'} and GET /students/7 тЖТ 200 {'id': '7', 'name': 'Aishwarya', 'class': '8B'} тАФ no phone.

Your snippet prints (400, {'message': 'Invalid request body', 'why': 'body must be a JSON object'}) for the list, (400, {'message': 'Invalid request body', 'why': "'mark' must be int"}) for 91.5, body must be a JSON object again for no body, then missing required field 'mark', and kitchen calls: 1 ┬╖ answer: {'id': '9', 'name': 'Katrina', 'class': '9A'}.

ЁЯПБ What you just proved

A bad form is refused at the desk with a reason, the kitchen is not called for it, and the answer can be cleaned on the way out without changing the kitchen.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ a model and a validator in OpenAPI for a REST API:

x-amazon-apigateway-request-validators:
  body-only: { validateRequestBody: true, validateRequestParameters: false }
paths:
  /marks:
    post:
      x-amazon-apigateway-request-validator: body-only
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [student, subject, mark]
              properties:
                student: { type: string }
                subject: { type: string }
                mark:    { type: integer, minimum: 0, maximum: 100 }

A response mapping template (VTL) on a custom Lambda integration that drops the phone:

#set($s = $input.path('$'))
{ "id": "$s.id", "name": "$s.name", "class": "$s.class" }

ЁЯПн Why this matters in production: request validation gives partners a clear 400 and keeps junk away from the database; response mapping is a last line against leaking fields. Test both with aws apigateway test-invoke-method.

тПня╕П Next

The desk works. How do you change it without breaking the real school day? Stages and deployments тАФ the rehearsal and the real show.

git checkout lesson-05-stages-deployments
тЖР Previousroutes integrationsNext тЖТstages deployments

This page is the lesson's README from the lesson-04-mapping-validation branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.