要件として、Django Adminのテキスト入力欄でYAML構文を直接編集し、DBにはプレーンなテキスト(TextField)として保存しつつ、アプリケーション内では自動的にPythonのdictやlistオブジェクトとして利用したいケースがあります。既存のサードパーティパッケージ(例:django-yamlfield)はDjango 4.x以降のAPI変更に対応しておらず、シリアライズ後の値が文字列のまま返されるなどの課題がありました。
以下は、models.Fieldを直接継承した再設計版です。これにより、フォーム・ウィジェット・バリデーション・シリアライズの全ライフサイクルを完全に制御可能になります。
核心コンポーネント
- YamlEditorWidget:Admin画面でYAML構文を整形表示・編集可能な
Textarea拡張 - YamlInputForm:入力値をYAML解析し、エラー時に明示的なバリデーションメッセージを提供
- YamlDataField:モデルフィールド本体。DBとの変換(
to_python/get_prep_value)、RESTフレームワーク連携(value_to_string)を統合管理
実装コード
import yaml
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.encoding import force_str
class YamlEditorWidget(forms.Textarea):
def render(self, name, value, attrs=None, renderer=None):
if value is None:
value = ""
elif not isinstance(value, str):
try:
value = yaml.safe_dump(
value,
default_flow_style=False,
allow_unicode=True,
indent=2,
width=120
)
except (yaml.YAMLError, TypeError):
value = str(value)
return super().render(name, value, attrs, renderer)
class YamlInputForm(forms.CharField):
empty_values = [None, ""]
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", YamlEditorWidget)
super().__init__(*args, **kwargs)
def to_python(self, raw_value):
if not isinstance(raw_value, str) or not raw_value.strip():
return raw_value
try:
parsed = yaml.safe_load(raw_value)
if parsed is None:
return {}
return parsed
except yaml.YAMLError as e:
raise ValidationError(f"Invalid YAML syntax: {e}")
def validate(self, value):
super().validate(value)
if value in self.empty_values and self.required:
raise ValidationError(self.error_messages["required"], code="required")
class YamlDataField(models.Field):
description = "Stores YAML-structured data as plain text, converts to Python objects on access"
def get_internal_type(self):
return "TextField"
def formfield(self, **kwargs):
defaults = {
"form_class": YamlInputForm,
"widget": YamlEditorWidget,
}
defaults.update(kwargs)
return super().formfield(**defaults)
def to_python(self, value):
if value is None:
return None
if isinstance(value, (dict, list)):
return value
if isinstance(value, bytes):
value = force_str(value)
if isinstance(value, str) and value.strip():
try:
return yaml.safe_load(value) or {}
except (yaml.YAMLError, ValueError):
return {}
return {}
def from_db_value(self, value, expression, connection):
return self.to_python(value)
def get_prep_value(self, value):
if value is None:
return None
try:
return yaml.safe_dump(
value,
default_flow_style=False,
allow_unicode=True,
indent=2,
width=120,
default_style=None
).strip()
except (yaml.YAMLError, TypeError):
return str(value)
def value_to_string(self, obj):
value = self.value_from_object(obj)
return self.get_prep_value(value)
def validate(self, value, model_instance):
super().validate(value, model_instance)
if value is not None:
try:
self.get_prep_value(value)
except (yaml.YAMLError, TypeError) as e:
raise ValidationError(f"Cannot serialize to YAML: {e}")
使用例
モデル定義:
class Configuration(models.Model):
name = models.CharField(max_length=100)
settings = YamlDataField(
help_text="YAML format only. E.g.,<br>- key: timeout<br> value: 30",
blank=True,
null=True
)
Adminでの表示は、YAML構文がインデント付きで可読性高くレンダリングされ、保存時にはバリデーション済みの構造化データとしてPythonコード内で利用可能です。
DRF(Django REST Framework)との連携も問題なく、シリアライザ経由でJSONレスポンスに自然に変換されます。