안녕하세요 오늘은 jsonschema를 통해서 사용자가 입력한 JSON데이타에 어떤 오류가 있는지를 확인하는 방법을 알려드릴게요.
초간단 validation
일단 우리가 사용할 모듈은 jsonschema입니다. import하시구요
import jsonschema
그 다음에 하셔야하는 일이 바로 schema를 정의하는 건데요. 여기서는 군더더기 다 빼고 그냥 기능만 아주 간단하게 알려드릴게요. 우선 여러분들이 입력받고자하는 사용자 입력데이타가 어떤건지 정의를 합니다. 저는 여기에서 우선 정수값의 ID를 받을 거구요, 이름은 문자열로 받을거에요.
SCHEMA = {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
}
이렇게 schema를 정의하셨으면 사용자 데이타를 가지고 한번 validate을 해볼게요. 이 사용자는 개발자가 시키는대로 id는 정수, 이름은 문자열로 잘 입력을 해주었어요.
data = {
"id": 1,
"name": "ellie"
}
그러면 이 사용자가 입력한 data가 위에 정의한 schema를 통과하는지 실행해봅니다.
jsonschema.validate(instance=data, schema=schema)
아무 문제가 없으면 이 함수는 아무것도 안하고 그냥 넘어갑니다. 그런데 만약에 어떤 사용자가 엉뚱하게도 id에 문자열을 넣고 name에 정수값을 넣었다고 해봅시다.
data = {
"id": "ellie",
"name": 1
}
jsonschema.validate(instance=data, schema=schema)
그러면 이때는 jsonschema.validate함수가 ValidationError 예외를 throw합니다.
Traceback (most recent call last):
File "/tmp/main.py", line 27, in <module>
jsonschema.validate(instance=data, schema=schema)
File "/usr/local/lib/python3.9/site-packages/jsonschema/validators.py", line 934, in validate
raise error
jsonschema.exceptions.ValidationError: 'ellie' is not of type 'integer'
Failed validating 'type' in schema['properties']['id']:
{'type': 'integer'}
On instance['id']:
'ellie'
그러면 사용자가 어떤 데이타를 넣을지 모르니까 jsonschema.validate() 를 호출할때는 반드시 try except로 묶어서 ValidationError exception이 발생했는지를 점검해 주어야겠죠?
Validation Error 모으기
위의 jsonschema.validate() 함수는 schema의 정의를 하나씩 살피면서 문제가 되는 데이터값을 발견했을때 바로 ValidationError를 throw합니다. 그런데 때로는 그때그때 하나씩 error를 고치기보다는 해당 입력 데이타에 어떤 어떤 에러가 있는지 한번에 확인하고 한번에 사용자에게 알려주거나 아니면 모아다가 다른 식으로 에러를 처리하고 싶을때가 있자나요. 그럴때는 jsonschema에서 제공하는 Draft7Validator라는 함수를 사용합니다.
예를 들어 저는 사용자입력 데이타를 싹 한번 훑은 다음에 문제가 되는 항목의 field명만 모아서 배열에 담고 싶어요. 그러면 아래와 같이 Draft7Validator를 호출해서 받은 validator로 data를 iter_errors의 인자로 넘겨주면 error를 전부다 한번에 받아올수가 있어요.
data = {
"id": "ellie",
"name": 1
}
validator = jsonschema.Draft7Validator(schema)
errors = validator.iter_errors(data)
invalid_fields = []
for error in errors:
invalid_fields.append(error.path[0])
errors에는 그러면 ValidationError가 배열로 담여있을거구요. 그 안에서 저는 field명만 빼서 invalid_fields에 담아줍니다. 그리고 나서 결과를 출력해보면 id와 name이 배열안에 담김니다.
print(invalid_fields)
['id', 'name']
invalid field 제거하기
이번에는 데이타를 받아서 확인하고 valid하지 않은 field들은 원본데이타에서 삭제해 볼게요. 마찬가지로 Draft7Validator를 이용할거에요.
def validate_schema(schema, data):
validator = jsonschema.Draft7Validator(schema)
errors = validator.iter_errors(data)
for error in errors:
remove_field(list(error.path), data)
아까 처럼 iter_errors를 호출해서 errors를 받아오는 부분까지는 동일해요. 그런데 이번에는 해당 path의 field를 data에서 제거하도록 하는 함수를 만들어서 아예 조건에 맞지 않는 데이타는 안받은척 하는거죠. remove_field에서는 path를 받아서 data와 함께 재귀적으로 호출을 하는데요. 이때 path의 가장 끝에 field명까지 갔다가 삭제하고 돌아오는 로직을 아래와 같이 구현합니다. 그래야 data의 depth가 여러개 일때 끝까지 찾아들어가서 지우고 오겠죠?
def remove_field(path, data):
field = path.pop(0)
if len(path) > 0:
remove_field(path, data[field])
if not data[field]:
data.pop(field)
else:
data.pop(field)
그럼 실행을 한번 해볼까요? schema를 multiple depth로 테스트 하기 위해서 name을 first와 last두개로 나눠서 정의할게요
schema = {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "object",
"properties": {
"first": {
"type": "string"
},
"last": {
"type": "string"
}
}
}
}
}
데이타는 완전 다 틀리게 넣어볼게요.
data = {
"id": "ellie",
"name": {
"first": 1,
"last": 2,
}
}
validate_schema(schema, data)
print(data)
결과를 보면 data가 전부 엉뚱한 데이타가 들어와서 data를 출력해보면 아무것도 없는 object인걸 확인하실수 있으실거에요
{}
그럼 이번에는 이름만 틀리게 해볼까요?
data = {
"id": 1,
"name": {
"first": 1,
"last": 2,
}
}
==> {'id': 1}
이름중에 하나만 틀리게 하면 어떨까요?
data = {
"id": 1,
"name": {
"first": "ellie",
"last": 2,
}
}
==> {'id': 1, 'name': {'first': 'ellie'}}
결과가 잘 나오는거 같네요. 여러분들도 한번 여러분들만의 코드를 만들어서 한번 실행해보세요. 이런것들이 알고나면 참 간단한데 배우기 전에는 헷갈리는 것들이죠. 배우지 않고도 자유롭게 코딩을 할수 있는 그 날이 올때까지 다같이 열심히 배워봅시다. 오늘도 여기까지 따라오느라 수고하셨습니다. 좋은 하루 되세요.
[Additional]
Validate Array
The validation result is a bit different when you have invalid data in an array.
schema = {
"type": "object",
"properties": {
"rates": {
"type": "array",
"items": {
"type": "number"
}
}
}
}
data = {'rates': ['a', 1, 'b']}
validator = jsonschema.Draft7Validator(schema)
errors = validator.iter_errors(data)
for error in errors:
print(error.path)
You will get below.
deque(['rates', 0])
deque(['rates', 2])
As you see, it will return the errors for each item.