Validate Data ยังไงให้ง่ายด้วย Cerberus

ปัญหานึงของการพัฒนาโปรแกรม โดยเฉพาะหลังจากโปรแกรมของเราเขียนเสร็จแล้ว จะเอาไปทำงานจริง มันจะมีเรื่องของข้อมูลที่เราเอาเข้ามา ที่แน่นอนว่า คนเขียนโปรแกรมบอกอย่าง แต่ Tester และ User ทำอย่าง จนทำให้กำหมัดกันรัว ๆ การทำ Data Validation มันช่วยลดปัญหาตรงนี้ได้เยอะมาก ๆ เลยทีเดียว ซึ่งใน Python เองมันก็มีหลาย ๆ Library ที่เข้ามาช่วย แต่ตัวนึงที่เราใช้บ่อยมาก ๆ คือ Cerberus ที่บอกเลยว่า ง่ายมาก ๆ เลยละ

ติดตั้ง Cerberus

pip install Cerberus

// or

conda install Cerberus

ก่อนที่เราจะใช้งานได้ เราจะต้องติดตั้งมันก่อน ง่าย ๆ เหมือนกับ Library บน Python ทั่ว ๆ ไปเลย คือเราสามารถติดตั้งผ่าน Pip หรือ Conda หรือ Package Manager ที่เราใช้งานได้เลย แต่ข้อสังเกตคือ ต้องใช้ C ใหญ่นะ ไม่งั้นมันจะหาไม่เจอเลยนะ เราลองกับ conda ที่ใช้ channel ของตัว conda-forge

Simple Validators

from cerberus import Validator

เรามาลองเล่นกันเลยว่าดีกว่า การที่เราจะ Validate Data ได้ เราจะต้องทำการสร้าง Object ของตัว Validator กันก่อน ซึ่งใน Cerberus จะใช้ Class ที่ชื่อว่า Validator ได้เลย

schema = {'name' : {'type' : 'string', 'required': True} , 'age': {'type': 'integer'}}

จากนั้น เราจะต้องกำหนดโครงสร้างของข้อมูลที่เราต้องการ ตัวอย่างด้านบน เราสร้าง Schema ที่ประกอบด้วย 2 Key คือ ชื่อ และอายุ โดยที่เราจะเห็นว่า ภายในเรากำหนด type อันนี้เป็น String และ Integer สำหรับชื่อ และ อายุตามลำดับ กับอีกอันที่เราเติมเข้ามาของ Name อย่างเดียว เป็นการบอกว่า Field name จะต้องใส่มาเสมอ ไม่งั้นไม่ผ่าน แต่ Age เราไม่ได้ใส่มา ดังนั้น ถ้าไม่ใส่มา มันก็ผ่านได้

val = Validator(schema)

จากนั้น เราทำการสร้าง Validator พร้อมกับใส่ Schema ที่เราสร้างไว้เข้าไป

data = {'name' : 'Arnon', 'age': 26}

if var.validate(data):
   print("Data is valid")
else:
   print("Data is invalid")
   print(var.errors)

จากนั้นเราก็มาลอง Validate กันดีกว่า เราสร้าง Data ปลอม ๆ ขึ้นมา สมมุติว่า เป็น Data ที่เราได้รับจาก User ขึ้นมา อาจจะมาเป็นรูปแบบของ JSON แล้วเราก็ Parse มันออกมาให้อยู่ในรูปแบบของ Dictionary มันก็จะเป็น data ปลอม ๆ ที่เราสร้างขึ้นมา จากนั้น เราก็จะมา Validate กันแล้ว โดยเรียก Method validate() จาก Validator ที่เราพึ่งสร้างขึ้นมา โดยที่ validate() จะคืนค่ากลับมาเป็น Boolean ถ้ามันตรงกัน ผ่านเงื่อนไข มันก็จะคืนเป็น True แต่ถ้าไม่ก็จะเป็น False

ส่วนถ้าคืนค่ากลับมาเป็น False เราก็ต้องอยากรู้ว่า ทำไมมันถึงไม่ผ่าน มันขาดหรืออะไรเกินมา โดยเรียก errors จาก Object ของ Validator ได้ตรง ๆ เลย ซึ่งเราก็จะได้กลับมาเป็น Dictionary ในรูปร่างเดียวกับ Schema ของเราเลย ซึ่งเราก็สามารถ Filter ออกมาแล้วดึง Error ออกมาเป็นชิ้น ๆ ได้อย่างง่าย ๆ อาจจะ Return เป็น Error ให้ User ได้

Load Schema From File

บางครั้ง การเขียน Schema ใน Code เลย มันก็อาจจะทำให้เวลา เรามีการเปลี่ยนแปลงโครงสร้างข้อมูลเป็นเรื่องยาก ทำให้อีกวิธีที่ทำให้ง่ายกว่าเดิมคือ เราสามารถเก็บ Schema ไว้ในไฟล์สักอย่าง อาจจะเป็นไฟล์ประเภทที่มี Structure หน่อยอย่าง YAML ก็ได้ง่ายดี

import yaml

with open('scheme.yaml', 'r') as scheme_file :
    schema = yaml.load(scheme_file.read())

val = Validator(schema)

จากตัวอย่างด้านบน เราทำอย่างง่ายเลยคือ เราทำการเปิดไฟล์ scheme.yaml ขึ้นมา แล้วให้มันอ่าน Schema ที่อยู่ในไฟล์ออกมา แล้วเราเรียกตัว Parser ที่ Built-in ในตัว Python เลย เพื่อแปลงไฟล์ Scheme ที่เราอ่านมาเป็น String ให้เป็น Dictionary พร้อมใช้ แล้วก็เรียกเข้า Validator เหมือนที่เราทำมาก่อนหน้าได้เลย จะเห็นเลยว่าง่ายมาก ๆ เลย และทำให้เราสามารถจัดการ Scheme ได้ง่ายขึ้นกว่าเดิมเยอะมาก

แต่ถ้าโปรแกรมเรา อยากจะปกปิดข้อมูลตรงนี้จริง ๆ เราอาจจะมีการ Encrypt ไปหน่อยก็ได้ แล้วอาจจะเปิดด้วย Private Key อะไรก็ว่ากันไปไม่รู้เหมือนกัน เพราะถ้าเราเขียน Python จริง ๆ มันก็ออกมาเป็น Script เห็นได้อยู่ดี

Required All & Allow Unknown

แต่ปัญหามันจะมาอีก เพราะถ้าเรากำหนด Schema ตรง ๆ เลยโดยพื้นฐาน ถ้าเราจะกำหนดให้ข้อมูลที่ใส่เข้ามาเป็น Schema เดียวกันเป๊ะ ๆ หมายความว่าทุก Field จะต้องมีข้อมูลเข้ามาหมด จากตัวอย่างก่อนหน้านี้ เราใช้ keyword ว่า required ไป ซึ่งถ้าเราอยากจะให้ทุก Field มันจำเป็นต้องใส่หมด เราก็ต้องไปใส่ required ในทุก ๆ ที่เลย ซึ่งมันเป็นอะไรที่รกมาก ทำให้ใน Cerberus เพิ่มอีกตัวเลือกเข้ามานั่นคือ required_all

val = Validator(schema, require_all=True)

ตอนที่เราสร้าง Validator เราสามารถ Pass argument require_all เข้าไปได้เลย โดยที่ถ้าเราบอกว่า True คือทุก ๆ Field มันจำเป็นที่จะต้องใส่เข้ามาเสมอ ไม่งั้นมันจะไม่ผ่าน

val = Validator(schema, allow_unknown=True)

นอกจากนั้น ถ้าเรา Strict ที่ Schema มาก ๆ บางที มันก็จะมีเคสที่มีปัญหาเหมือนกัน เราจะต้องอนุญาติให้ใส่อะไรบางอย่างเข้ามาเพิ่มได้ด้วย แต่เราไม่สามารถกำหนดใน Scheme ได้ มันก็จะมี Argument ใน Validator อีกเหมือนกันชื่อว่า allow_unknown ถ้าเราใส่ True หมายความว่า มันจะอนุญาติให้ User ใส่อะไรที่ไม่มีอยู่ใน Schema ออกมาได้ด้วย

แต่ ๆ การทำแบบนี้ก็ไม่น่ารักอีก เพราะการที่เราให้อะไรไม่รู้เข้ามาแล้ว มันก็แย่แล้ว แต่ถ้าให้เข้ามา มันก็ควรจะถูกควบคุมหน่อย เช่น เราอาจจะบอกว่า เราสามารถใส่เข้ามาเป็น String ได้เท่านั้น เราก็สามารถทำได้เช่นกัน

val.allow_unknown = {'type': 'string'}

สิ่งที่เราทำคือ เราทำการกำหนด Attribute บน Validator ที่ชื่อว่า allow_unknown เป็นตัว Rule ได้เลยว่า เราต้องการกำหนดให้มันเป็นอะไร จากตัวอย่างด้านบน เราก็จะกำหนดให้มันเป็น String เท่านั้น ถ้า Field ไหนที่เราไม่ได้กำหนดเข้ามา มันจะรับมาแค่ String เท่านั้น ไม่งั้น มันก็จะดีดออกตอนที่เรา Validate นั่นเอง

สรุป

Cerberus เข้ามาช่วยเราในการ Validate Data ที่รับเข้ามาได้ง่าย ๆ เลยทำให้เราล่นเวลาในการทำงานที่ต้องมานั่งเช็ค Input ก่อนที่เราจะเอาไปทำอะไรทุกครั้ง โดยตัวอย่างที่เราเอามาให้ดูในวันนี้เป็นตัวอย่างสั้น ๆ เล็ก ๆ เท่านั้น เวลาเราเอาไปใช้จริง ๆ อาจจะมี Schema ที่ซับซ้อน และมี Rule มากกว่านี้ ลองไปหาอ่านได้ใน Documentation ของ Cerberus ได้เลย