概要
AWS CloudFormation の template.yaml を JSON に変換できないかな…と思って、変換方法を色々試した。
結果、次のことが分かった。
- PyYAML は
YYYY-MM-DD
を見つけると datetime.date として読み込むので何もせずにJSON出力すると失敗する
→ これは何とかなるので別にいい - AWS の YAML と JSON でキー名の慣例が違う (YAML: PascalCase, JSON: CamelCase)し、そもそも統一されていない
→ 自動変換するツールを作るのは無理。というのは AWS側の仕様で決めたキー名と利用者が決めた名前のキーでルールがまちまちだから。 - そもそも YAML は表現を保存しようとは思っていない
最後の点を取り上げようと思う。
YAML は表現方法を保存しようとは思っていない
YAMLの仕様で書式は定められているが、それに従うとその書式から解析した値を取り込む。
時刻のようなものの扱い
YAMLの仕様で :
区切りの時刻のようなものは 60進数として扱うので、PyYAML もそれに従って数値として取り込む。そのため、時刻書式という表現は保存されない。Python の datetime.time は 0-23時の範囲で入力制限があるが YAML のこの書式は 23より大きな値も入る。
>>> import yaml
>>> yaml.safe_load('Time: 1:01')
{'Time': 61}
>>> yaml.safe_load('Time: 1:00:00')
{'Time': 3600}
数値の扱い
16進数や8進数も数値として取り込む。もともとが 8進数や16進数であったことは忘れて整数になる。
>>> yaml.safe_load('hex: 0xC')
{'hex': 12}
>>> yaml.safe_load('hex: 077')
{'hex': 63}
>>> 7*8+7
63
>>>
日付時刻の扱い
ISO8601の標準的な形式以外に日付時刻の間に空白をいれた形式も理解する。ポイントは datetime オブジェクトで解釈することで、JSON変換するような場合に T
で区切ってあったのか
で区切ってあったのかを忘れるということ。
>>> yaml.safe_load('2021-07-27T10:10:10')
datetime.datetime(2021, 7, 27, 10, 10, 10)
>>> yaml.safe_load('2021-07-27 10:10:10')
datetime.datetime(2021, 7, 27, 10, 10, 10)
>>>
変換ツール
- Python 3.8.x
- Ubuntu 18.04 LTS
素朴な JSON変換
もっと分かりにくく短く書くことはできるのですが、まぁ、これで。
"""Convert yaml to json."""
import json
import datetime
import sys
import typing
import yaml
def json_dumper(obj: typing.Any) -> typing.Any:
"""Dump json."""
if isinstance(obj, datetime.date):
# date and datetime
return obj.isoformat()
else:
return obj
def yaml_to_json(yamlFileName: str) -> str:
"""Convert yaml to json"""
with open(yamlFileName, 'rb') as f:
d = yaml.safe_load(f)
s = json.dumps(
d,
default=json_dumper,
indent=4
)
return s
if __name__ == '__main__':
print(yaml_to_json(sys.argv[1]))
# EOF
日付の変換をやめさせる
日付程度なら実は上のコードで十分なのだが、Loaderを使った例があまりないので掲載しておく。
日付書式の文字列をそのまま JSON 側に適用してくれる。
なお、 yaml.load(xxx)
のように Loader を省略すると deprecated の警告が出る。 yaml.safe_load
はローダーがハードコーディングされた形式となっている。
"""Convert yaml to json."""
import json
import sys
import typing
import yaml
class NoDatesSafeLoader(yaml.SafeLoader):
"""Disable SafeLoader.
https://stackoverflow.com/questions/34667108/ignore-dates-and-times-while-parsing-yaml
"""
@classmethod
def remove_implicit_resolver(cls, tag_to_remove: str) -> typing.NoReturn:
"""
Remove implicit resolvers for a particular tag
Takes care not to modify resolvers in super classes.
We want to load datetimes as strings, not dates, because we
go on to serialize as json which doesn't have the advanced types
of yaml, and leads to incompatibilities down the track.
"""
if 'yaml_implicit_resolvers' not in cls.__dict__:
cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy()
for first_letter, mappings in cls.yaml_implicit_resolvers.items():
cls.yaml_implicit_resolvers[first_letter] = [
(tag, regexp) for tag, regexp in mappings if tag != tag_to_remove
]
def yaml_to_json(yamlFileName: str) -> str:
"""Convert yaml to json"""
NoDatesSafeLoader.remove_implicit_resolver('tag:yaml.org,2002:timestamp')
with open(yamlFileName, 'rb') as f:
d = yaml.load(f, Loader=NoDatesSafeLoader)
return json.dumps(d, indent=4)
if __name__ == '__main__':
print(yaml_to_json(sys.argv[1]))
# EOF
テストデータ
# Date and Time
DateTime:
Time:
time: 20:03:47
Timestamps:
canonical: 2001-12-15T02:59:43.999999Z
Date: 2012-10-17
dt spaced 1: 2012-10-17 00:00:00
dt spaced 2: 2012-10-17 23:59:00
dt spaced 3: 2012-10-17 23:59:00 -1
dt iso8601 1: 2012-10-17t23:59:00+09:00
dt iso8601 2: 2012-10-17T23:59:00-01:00
Variables:
ArrayStr:
- Foo
- Bar
- Hello
World
- |
Hello
World
Integers:
canonical: 12345
decimal: +12345
octal: 02472256 # 685230
hexadecimal: 0xC # 12
base 60: 190:20:30 # 685230
Floats:
pi: 3.14159265358979323846264338327950
fixed: 685_230.15
canonical: 6.8523015e+5
exponential: 685.230_15e+03
Not a number: .NaN
Inf: .inf
NegativeInf: -.inf
Booleans:
- true
- false
Null: null
Sequence of Sequences:
- [Apple, 200]
- [Banana, 100]
Resources:
SNSTopic:
Type: AWS::SNS::Topic
Condition: CreateResources
Properties:
TopicName: foo-topic