API를 설계하면서 JSON 형식의request로 들어오는 parameter들의 유효성 검사를 체크하기 위해 jsonschema를 프로젝트에 적용해보기로 했다.
JSON의 유효성을 검사하고 주석을 추가하고 조작할 수 있는 라이브러리이다. JSON 스키마를 활용하면 API의 parameter에 들어갈 데이터의 형식을 지정하고 API 내부적으로는 해당 request의 형식이 올바른지 검증할 수 있다.
다음과 같은 명령어를 사용하여 jsonschema를 설치해준다.
$ pip install jsonschema
프로젝트에서 셀러를 등록하는 api의 sample request를 작성해준다.
{
"profile": "url",
"background_image": "url",
"simple_introduction": "안녕 난 진아야 안녕",
"detail_introduction": "셀러 상세 소개",
"site_url": "http://www.naver.com",
"service_number": "010-5338-7244",
"zip_code": "우편번호",
"address": "주소 (택배 수령지)",
"detail_address": "상세주소 (택배 수령지)",
"bank": "정산은행",
"account_owner": "계좌주",
"bank_account": "계좌번호",
"shipping_information": "배송정보",
"refund_information": "교환 / 환불 정보",
"model_height": 177,
"model_size_top": 50,
"model_size_bottom": 30,
"model_size_foot": 255,
"feed_message": "안녕하세요 OOO에요! 봄에 어울리는 신상이 입고 되었습니다."
}
https://jsonschema.net 이라는 사이트에서는 request에 들어갈 json을 넣게 되면 해당 request에 에 적합한 json schema를 추출해준다.
좋은 세상이다...
이제 추출된 json schema를 에디터로 긁어와 자신의 입맛에 따라 가공해준다. 물론 해당 사이트에서 json schema가 추출된 에디터 좌측 상단의 연필모양의 버튼을 누르면 UI로 직접 편집이 가능하다.
추출된 스키마는 다음과 같다
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"default": {},
"examples": [
{
"profile": "url",
"background_image": "url",
"simple_introduction": "안녕 난 진아야 안녕",
"detail_introduction": "셀러 상세 소개",
"site_url": "http://www.naver.com",
"service_number": "010-5338-7244",
"zip_code": "우편번호",
"address": "주소 (택배 수령지)",
"detail_address": "상세주소 (택배 수령지)",
"bank": "정산은행",
"account_owner": "계좌주",
"bank_account": "계좌번호",
"shipping_information": "배송정보",
"refund_information": "교환 / 환불 정보",
"model_height": 177,
"model_size_top": 50,
"model_size_bottom": 30,
"model_size_foot": 255,
"feed_message": "안녕하세요 OOO에요! 봄에 어울리는 신상이 입고 되었습니다."
}
],
"required": [
"simple_introduction",
"site_url",
"service_number",
"detail_address",
"bank",
"account_owner",
"bank_account",
"shipping_information",
"refund_information",
"model_height"
],
"additionalProperties": true,
"properties": {
"profile": {
"$id": "#/properties/profile",
"type": "string",
"title": "The profile schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"url"
]
},
"background_image": {
"$id": "#/properties/background_image",
"type": "string",
"title": "The background_image schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"url"
]
},
"simple_introduction": {
"$id": "#/properties/simple_introduction",
"type": "string",
"title": "The simple_introduction schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"안녕 난 진아야 안녕"
]
},
"detail_introduction": {
"$id": "#/properties/detail_introduction",
"type": "string",
"title": "The detail_introduction schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"셀러 상세 소개"
]
},
"site_url": {
"$id": "#/properties/site_url",
"type": "string",
"title": "The site_url schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"http://www.naver.com"
],
"pattern": "(http|https):\\/\\/(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(\\/|\\/([\\w#!:.?+=&%@!\\-\\/].[^\\s]*$))?"
},
"service_number": {
"$id": "#/properties/service_number",
"type": "string",
"title": "The service_number schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"010-5338-7244"
],
"pattern": "(02.{0}|^01.{1}|[0-9]{4})-([0-9]+)-([0-9]{4})"
},
"zip_code": {
"$id": "#/properties/zip_code",
"type": "string",
"title": "The zip_code schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"우편번호"
]
},
"address": {
"$id": "#/properties/address",
"type": "string",
"title": "The address schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"주소 (택배 수령지)"
]
},
"detail_address": {
"$id": "#/properties/detail_address",
"type": "string",
"title": "The detail_address schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"상세주소 (택배 수령지)"
],
"minLength": 1
},
"bank": {
"$id": "#/properties/bank",
"type": "string",
"title": "The bank schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"정산은행"
]
},
"account_owner": {
"$id": "#/properties/account_owner",
"type": "string",
"title": "The account_owner schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"계좌주"
]
},
"bank_account": {
"$id": "#/properties/bank_account",
"type": "string",
"title": "The bank_account schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"계좌번호"
],
"pattern": ""
},
"shipping_information": {
"$id": "#/properties/shipping_information",
"type": "string",
"title": "The shipping_information schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"배송정보"
]
},
"refund_information": {
"$id": "#/properties/refund_information",
"type": "string",
"title": "The refund_information schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"교환 / 환불 정보"
]
},
"model_height": {
"$id": "#/properties/model_height",
"type": "integer",
"title": "The model_height schema",
"description": "An explanation about the purpose of this instance.",
"default": 0,
"examples": [
177
]
},
"model_size_top": {
"$id": "#/properties/model_size_top",
"type": "integer",
"title": "The model_size_top schema",
"description": "An explanation about the purpose of this instance.",
"default": 0,
"examples": [
50
]
},
"model_size_bottom": {
"$id": "#/properties/model_size_bottom",
"type": "integer",
"title": "The model_size_bottom schema",
"description": "An explanation about the purpose of this instance.",
"default": 0,
"examples": [
30
]
},
"model_size_foot": {
"$id": "#/properties/model_size_foot",
"type": "integer",
"title": "The model_size_foot schema",
"description": "An explanation about the purpose of this instance.",
"default": 0,
"examples": [
255
]
},
"feed_message": {
"$id": "#/properties/feed_message",
"type": "string",
"title": "The feed_message schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"안녕하세요 OOO에요! 봄에 어울리는 신상이 입고 되었습니다."
]
}
}
}
required
: 필수적으로 필요한 입력값을 강제할 수 있다.
pattern
: 원하는 정규식을 대입할 수 있다.
default
: 말 그대로 default 값을 지정해준다.
minLength
: parameter 길이의 최솟값을 지정해준다. 반대로 'maxLength'도 사용할 수 있다.
우선 추출한 json schema를 프로젝트의 config.py에 넣어줬다. config.py는 데이터베이스 커넥션에 필요한 정보를 담은 db connection, 토큰 해쉬에 필요한 secret key, algorithm 세팅과 같은 민감한 정보를 가지고 있기 때문에 .gitignore에 추가해준 파이썬 파일이다. 여기서 방금 추출한 json schema를 가지고 왔다.
config.py
컨트롤러 레이어에서 validation을 적용해보자. 우선 사용자 컨트롤러를 담당하는 user_controller.py
에서 json schema와 jsonschema 라이브러리의 validate 메소드, 예외처리에 필요한 ValidationError를 import 해준다.
from jsonschema import validate, ValidationError
그리고 내가 적용하고자 하는 API 메소드에 validate 메소드와 ValidationError 예외처리 구문을 추가한다. validate 메소드에 들어가는 parameter는 다음과 같다
validate(API request, 사용할 json schema)
def update_seller():
db_connection = None
seller_infos = request.json
try:
validate(seller_infos, seller_register_schema)
db_connection = get_connection()
if db_connection:
register_response = user_service.update_seller(g.user, seller_infos, db_connection)
db_connection.commit()
return register_response
except ValidationError as e:
return {'message' : str(e)}, 400
과도하게 친철하다고 생각될만큼 에러메세지가 자세하다.
참고사이트 :
http://blog.weirdx.io/post/35740
https://json-schema.org/
https://github.com/mcchae/JSON-Schema/blob/master/JSON-API.md