LoginSignup
8

More than 1 year has passed since last update.

Kotlin Serialization ガイド 第5章 JSONの機能

Last updated at Posted at 2021-02-26

はじめに

 Kotlin Serialization ガイドの目次に基づいて「Kotlin Serialization guide」を訳しています。
 なお、翻訳にはDeepLの力を99%借りています。
 もし、一緒に「Kotlin Serialization ガイド」の翻訳をして下さる方がいらっしゃいましたら、コメント欄などから連絡をください。

第5章 JSONの機能<原文

Latest commit 728e220 on 24 Nov 2020版

Kotlin Serialization Guideの第5章です。
この章では、さまざまな Json の機能について解説します。

目次

Jsonの設定

 デフォルトでは、Jsonの実装は無効な入力に対して非常に厳しく、Kotlinの型の安全性を確保しています。はシリアライズできるKotlinの値を制限し、結果として得られるJSON表現が標準的なものになるようにします。多くの非標準JSON機能は、JSON formatのカスタムインスタンスを作成することでサポートされています。デフォルトでは、Jsonの実装は無効な入力に対してかなり厳しく、Kotlinの型の安全性を強制し、シリアライズできるKotlinの値を制限して、結果として得られるJSON表現が標準的なものになるようにしています。多くの非標準のJSON機能は、JSON formatのカスタムインスタンスを作成することでサポートされています。

 JSON形式の設定は、デフォルトの Json オブジェクトや Json() ビルダー関数など、既存のインスタンスを利用して独自の Jsonクラスのインスタンスを作成することで指定することができます。追加のパラメータはJsonBuilder DSLを介してブロックで指定します。結果として得られる Json 形式のインスタンスは不変でスレッドセーフです。これは単にトップレベルのプロパティに格納することができます。

パフォーマンス上の理由から、フォーマットのカスタムインスタンスを保存して再利用することを推奨します。

 この章では、Jsonがサポートする様々な設定機能を紹介します。 

綺麗な印刷

 JSONは、prettyPrintプロパティを設定することで、出力をきれいに印刷するように設定することができます。

val format = Json { prettyPrint = true }

@Serializable 
data class Project(val name: String, val language: String)

fun main() {                                      
    val data = Project("kotlinx.serialization", "Kotlin")
    println(format.encodeToString(data))
}

完全なコードはこちらから取得できます,。

 以下のような良い結果が得られます。

{
    "name": "kotlinx.serialization",
    "language": "Kotlin"
}

簡潔な構文解析

 デフォルトでは、Jsonパーサは、できるだけ仕様に準拠するように、様々なJSONの制限を強制します(RFC-4627を参照してください)。キーは引用符で囲まなければならず、リテラルは引用符で囲まれていないものとする。これらの制限は、 isLenient プロパティで緩和することができます。isLenient = true とすると、かなり自由なフォーマットのデータを解析することができます。

val format = Json { isLenient = true }

enum class Status { SUPPORTED }                                                     

@Serializable 
data class Project(val name: String, val status: Status, val votes: Int)

fun main() {             
    val data = format.decodeFromString<Project>("""
        { 
            name   : kotlinx.serialization,
            status : SUPPORTED,
            votes  : "9000"
        }
    """)
    println(data)
}

フルコードはこちらから取得することができます。

 整数が引用符で囲まれていたのに、キー、文字列、enumの値がすべて引用符で囲まれていないのに、オブジェクトを取得しています。

Project(name=kotlinx.serialization, status=SUPPORTED, votes=9000)

未知のキーを無視

 JSON形式は、サードパーティのサービスの出力を読み取るためによく使用されますが、そうでなければ、APIの進化の一環として新しいプロパティが追加される可能性があるような、非常にダイナミックな環境でも使用されます。デフォルトでは、デシリアライズ中に未知のキーが検出されるとエラーが発生します。この動作は、ignoreUnknownKeysプロパティで設定できます。

val format = Json { ignoreUnknownKeys = true }
@Serializable 
data class Project(val name: String)

fun main() {             
    val data = format.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(data)
}

フルコードはこちらから取得することができます。

 これはオブジェクトが language プロパティを欠いているにもかかわらず、オブジェクトをデコードします。

Project(name=kotlinx.serialization)

入力値の強制

 野生(wild)で遭遇するJSONフォーマットは、型の面で柔軟性があり、すぐに進化することができます。これは、実際の値が期待値と一致しない場合に、デコード中に例外が発生する可能性があります。デフォルトでは、Json の実装は、型安全性の確保 のセクションで実証されたように、入力型に関しては厳格です。これは coerceInputValues プロパティを使用して多少緩和することができます。 このプロパティは、デコードにのみ影響します。これは、無効な入力値の限られたサブセットを、対応するプロパティがないかのように扱い、代わりに対応するプロパティのデフォルト値を使用します。
 現在サポートされている無効な値のリストは以下の通りです。

  • NULL不可能な型のための null 入力。
  • 列挙型の値が不明です。

このリストは将来的に拡張される可能性があり、このプロパティで設定されたJsonインスタンスは、入力中の無効な値に対してさらに寛容になり、それらをデフォルト値に置き換えるようになります。

 ここでは、型安全性の確保の部分を例にしてみます。

val format = Json { coerceInputValues = true }

@Serializable 
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = format.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","language":null}
    """)
    println(data)
}

フルコードはこちらからすることができます。

 これにより、language プロパティの無効な null 値がデフォルト値に強制的に変換されたことがわかります。

Project(name=kotlinx.serialization, language=Kotlin)

エンコードのデフォルト値

 プロパティのデフォルト値はデフォルトではエンコードされていません。詳細は、デフォルトがエンコードされていないの項を参照してください。これは、NULLデフォルトを持つNULL可能なプロパティに対して特に有用であり、対応するNULL値の書き込みを避けることができます。デフォルトの動作は、encodeDefaultsプロパティで変更できます。

val format = Json { encodeDefaults = true }

@Serializable 
class Project(
    val name: String, 
    val language: String = "Kotlin",
    val website: String? = null
)           

fun main() {
    val data = Project("kotlinx.serialization")
    println(format.encodeToString(data))
}

フルコードはこちらから取得することができます。

 これは、すべてのプロパティの値をエンコードした以下の出力を生成します。

{"name":"kotlinx.serialization","language":"Kotlin","website":null}

構造化されたマップキーの使用許可

 JSONフォーマットは、構造化されたキーを持つマップの概念をネイティブにサポートしていません。JSON オブジェクトのキーは文字列であり、デフォルトではプリミティブや列挙型のみを表現するために使用することができます。JSON フォーマットは、構造化されたキーを持つマップの概念をネイティブにサポートしていません。JSON オブジェクトのキーは文字列であり、デフォルトではプリミティブや列挙型のみを表現するために使用することができます。

 構造化キーの非標準サポートは、allowStructuredMapKeysプロパティを使用して有効にすることができます。

val format = Json { allowStructuredMapKeys = true }

@Serializable 
data class Project(val name: String)

fun main() {             
    val map = mapOf(
        Project("kotlinx.serialization") to "Serialization",
        Project("kotlinx.coroutines") to "Coroutines"
    )
    println(format.encodeToString(map))
}

フルコードはこちらから取得することができます。

 構造化キーを持つマップは [key1, value1, key2, value2,...] のJSON配列として表現されます。

[{"name":"kotlinx.serialization"},"Serialization",{"name":"kotlinx.coroutines"},"Coroutines"]

特殊な浮動小数点値の許可

 デフォルトでは、JSON仕様では禁止されているため、Double.NaNや無限大などの特殊な浮動小数点値はJSONではサポートされていません。しかし、allowSpecialFloatingPointValuesプロパティを使用して有効にすることができます。

val format = Json { allowSpecialFloatingPointValues = true }

@Serializable
class Data(
    val value: Double
)                     

fun main() {
    val data = Data(Double.NaN)
    println(format.encodeToString(data))
}

フルコードはこちらから取得することができます。

 この例では、以下のような非標準のJSON出力を生成していますが、JVMの世界では特殊な値のために広く使われているエンコーディングです。

{"value":NaN}

クラス判別器

 多相データを持っているときに型を指定するキー名は、classDiscriminatorプロパティで指定することができます。

val format = Json { classDiscriminator = "#class" }

@Serializable
sealed class Project {
    abstract val name: String
}

@Serializable         
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}  

フルコードはこちらから取得することができます。

 クラスの明示的に指定された SerialName と組み合わせることで、結果として得られる JSON オブジェクトを完全に制御することができます。

{"#class":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

Json要素

 これまでは、オブジェクトを文字列に変換したり戻したりしてJSON形式を扱ってきました。しかし、JSONは実際には非常に柔軟性に富んでいることが多く、このような構造化されていないデータを扱う場合は、Kotlinのシリアライズというタイプセーフの世界にはなかなか馴染まないため、パースなどの作業をする前にデータを微調整する必要があるかもしれません。

Json要素への構文解析

 文字列は、Json.parseToJsonElement関数でJsonElementのインスタンスに解析することができます。デコードもデシリアライズもしない、というのは、その過程で何も起こらないからです。ここではJSONパーサー(構文解析)のみを使用しています。

fun main() {
    val element = Json.parseToJsonElement("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(element)
}

フルコードはこちらから取得することができます。

 JsonElement` はそれ自身を有効なJSONとして表示します。

{"name":"kotlinx.serialization","language":"Kotlin"}

Json要素のサブタイプ

 JsonElementクラスは、JSON文法に密接に従った3つの直接的なサブタイプを持っています。

  • JsonPrimitiveは、文字列、数値、ブール値、ヌル値など、すべてのプリミティブなJSON要素を表します。各プリミティブには、単純な文字列contentがあります。また、様々なKotlinプリミティブ型を受け入れて JsonPrimitive に変換するためにオーバーロードされた JsonPrimitive()コンストラクタ関数もあります。

  • JsonArray は JSON [....] の配列を表す。JsonElement`のKotlinのListである。

  • JsonObject は JSON の {...} オブジェクトを表す。これは、String キーから JsonElement 値へのKotlinのMapである。

 JsonElementクラスはjsonXxx` の拡張子を持ち、それを対応するサブタイプ ( jsonPrimitive, jsonArray, jsonObject ) にキャストします。

 JsonPrimitiveクラスには、Kotlinのプリミティブ型(int, intOrNull, long, longOrNullなど)への便利な変換器があり、構造を知っているJSONを使って流暢なコードを書くことができます。

fun main() {
    val element = Json.parseToJsonElement("""
        {
            "name": "kotlinx.serialization",
            "forks": [{"votes": 42}, {"votes": 9000}, {}]
        }
    """)
    val sum = element
        .jsonObject["forks"]!!
        .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
    println(sum)
}

フルコードはこちらから取得することができます。

 上の例では、forks 配列内のすべてのオブジェクトの votes を合計しています。

9042

Json 要素のビルダー

 特定のJsonElementサブタイプのインスタンスは、それぞれのビルダー関数buildJsonArraybuildJsonObjectを用いて構築することができます。これはKotlin標準のライブラリ・コレクション・ビルダーに似ていますが、JSON固有の利便性である型固有のオーバーロードや内部ビルダー関数を追加した構造を定義するDSLを提供します。以下の例は、すべての主要な機能を示しています。

fun main() {
    val element = buildJsonObject {
        put("name", "kotlinx.serialization")
        putJsonObject("owner") {
            put("name", "kotlin")
        }
        putJsonArray("forks") {
            addJsonObject {
                put("votes", 42)
            }
            addJsonObject {
                put("votes", 9000)
            }
        }
    }
    println(element)
}

フルコードはこちらから取得することができます。

 最後に、適切なJSON文字列を取得します。

{"name":"kotlinx.serialization","owner":{"name":"kotlin"},"forks":[{"votes":42},{"votes":9000}]}

Json要素のデコード

 JsonElement クラスのインスタンスは、Json.decodeFromJsonElement 関数を使ってシリアライズ可能なオブジェクトにデコードすることができます。

@Serializable 
data class Project(val name: String, val language: String)

fun main() {
    val element = buildJsonObject {
        put("name", "kotlinx.serialization")
        put("language", "Kotlin")
    }
    val data = Json.decodeFromJsonElement<Project>(element)
    println(data)
}

フルコードはこちらから取得することができます。

 期待通りの結果になっています。

Project(name=kotlinx.serialization, language=Kotlin)

Json変換

 シリアライズ後のJSON出力の形状や内容に影響を与えたり、入力をデシリアライズに適応させたりするには、カスタムシリアライザーを記述することが可能です。しかし、EncoderDecoderの呼び出し規則に注意して従うことは、特に比較的小さくて簡単な作業の場合には便利ではないかもしれません。そのために、Kotlinのシリアライズでは、カスタムシリアライザを実装する際の負担を、Jsonの要素ツリーを操作する問題にまで軽減できるAPIを提供しています。

 カスタムシリアライザがどのようにクラスにバインドされるかなどについて説明しているので、シリアライザの章に精通しておくことを強くお勧めします。

 変換機能は、KSerializerを実装した抽象的なJsonTransformingSerializerクラスによって提供されます。このクラスは、EncoderDecoderと直接対話する代わりに、transformSerializetransformDeserializeメソッドを使って、JsonElementクラスで表現されるJSONツリーの変換を依頼します。例を見てみましょう。

配列の折り返し(ラッピング)

 最初の例は、リストのJSON配列ラッピングを独自に実装したものです。User オブジェクトの JSON 配列を返す REST API を考えてみましょう。データモデルでは、users.List<User> プロパティにカスタムシリアライザを指定するために、@Serializable アノテーションを使用しています。List` プロパティを使用します。

@Serializable 
data class Project(
    val name: String,
    @Serializable(with = UserListSerializer::class)      
    val users: List<User>
)

@Serializable
data class User(val name: String)

 今のところ、我々はデシリアライズのみに関心があるので、UserListSerializerを実装し、transformDeserialize関数のみをオーバーライドします。JsonTransformingSerializer` のコンストラクタはオリジナルのシリアライザをパラメータとして受け取り、ここでは コレクションシリアライザを構築 のセクションのアプローチを利用して作成します。

object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
    // If response is not an array, then it is a single object that should be wrapped into the array
    override fun transformDeserialize(element: JsonElement): JsonElement =
        if (element !is JsonArray) JsonArray(listOf(element)) else element
}

 これで、JSON 配列や単一の JSON オブジェクトを入力としてコードをテストすることができるようになりました。

fun main() {     
    println(Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
    """))
    println(Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
    """))
}

フルコードはこちらから取得することができます。

 出力を見ると、両方のケースが正しくKotlin Listにデシリアライズされていることがわかります。

Project(name=kotlinx.serialization, users=[User(name=kotlin)])
Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])

配列の展開(アンラッピング)

 シリアライズ中に単一要素のリストを単一のJSONオブジェクトにアンラップするために transformSerialize 関数を実装することもできます。

    override fun transformSerialize(element: JsonElement): JsonElement {
        require(element is JsonArray) // we are using this serializer with lists only
        return element.singleOrNull() ?: element
    }

 さて、Kotlinでオブジェクトの単一要素のリストから始める。

fun main() {     
    val data = Project("kotlinx.serialization", listOf(User("kotlin")))
    println(Json.encodeToString(data))
}

フルコードはこちらから取得することができます。

 結局、1つのJSONオブジェクトになってしまいます。

{"name":"kotlinx.serialization","users":{"name":"kotlin"}}

デフォルト値の操作

 もう一つの有用な変換の種類は、出力JSONから特定の値を省略することです。例えば、欠落している場合やその他のドメイン固有の理由でデフォルトとして扱われるためです。例えば、Project データモデルでは language プロパティにデフォルト値を指定できないが、Kotlin と等しい場合には JSON から省略しなければならないとします (いずれにせよ Kotlin がデフォルトであるべきであることは誰もが認めるところです)。Projectクラス用のプラグイン生成ジェネリックシリアライザをベースにした特別なProjectSerializerを記述することで修正する。

@Serializable
class Project(val name: String, val language: String)

object ProjectSerializer : JsonTransformingSerializer<Project>(Project.serializer()) {
    override fun transformSerialize(element: JsonElement): JsonElement =
        // Filter out top-level key value pair with the key "language" and the value "Kotlin"
        JsonObject(element.jsonObject.filterNot {
            (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
        })
}                           

 以下の例では、Projectクラスをトップレベルでシリアライズしているので、シリアライザを手動で渡すで示したように、上記のProjectSerializerを明示的にJson.encodeToString関数に渡しています。

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(Json.encodeToString(data)) // using plugin-generated serializer
    println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer
}

フルコードはこちらから取得することができます。カスタムシリアライザの効果がよくわかります。

{"name":"kotlinx.serialization","language":"Kotlin"}
{"name":"kotlinx.serialization"}

コンテンツベースのポリモーフィックの逆シリアライゼーション

 通常、ポリモフィズムおけるシリアル化では、Kotlinクラスをデシリアライズするために使用されるべき実際のシリアライザを決定するために、JSONオブジェクトに専用の"typeキー(クラス判別キーとしても知られています)が必要になります。
 しかし、入力にtypeプロパティが存在しない場合もあり、特定のキーの存在などJSONの形状から実際の型を推測することが期待されます。
 JsonContentPolymorphicSerializerは、そのような戦略のためのスケルトン実装を提供します。これを使うには、selectDeserializerメソッドをオーバーライドする。まずは以下のようなクラス階層から見ていきましょう。

注:シールドされたクラス のセクションで推奨されているように シールドされている必要はありません。なぜなら、適切なサブクラスを自動的に選択するプラグイン生成コードを利用するのではなく、このコードを手動で実装しようとしているからです。

@Serializable
abstract class Project {
    abstract val name: String
}                   

@Serializable 
data class BasicProject(override val name: String): Project() 

@Serializable
data class OwnedProject(override val name: String, val owner: String) : Project()

 JSONオブジェクトに owner キーがあることで BasicProjectOwnedProject のサブクラスを区別したいと思います。

object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) {
    override fun selectDeserializer(element: JsonElement) = when {
        "owner" in element.jsonObject -> OwnedProject.serializer()
        else -> BasicProject.serializer()
    }
}

 このようなシリアライザでデータをシリアライズすることができます。その場合、実行時には登録されたもの(registered)か、実際の型に合わせたデフォルトのシリアライザが選択されます。

fun main() {
    val data = listOf(
        OwnedProject("kotlinx.serialization", "kotlin"),
        BasicProject("example")
    )
    val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
    println(string)
    println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
}

フルコードはこちらから取得することができます。

 JSON出力にはクラス判別器は追加されていません。

[{"name":"kotlinx.serialization","owner":"kotlin"},{"name":"example"}]
[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]

フード下(実験的なもの)

 上記の抽象シリアライザはほとんどのケースをカバーすることができますが、KSerializerクラスのみを使用して、同様の機械を手動で実装することも可能です。抽象メソッド transformSerialize/transformDeserialize/selectDeserializer を微調整するだけでは不十分な場合は、serialize/deserializeを変更するのがよいでしょう。Jsonを使ったカスタムシリアライザに関する豆知識がいくつかあります。

 以上のことから、Decoder -> JsonElement -> valuevalue -> JsonElement -> Encoder の二段階変換を実装することが可能です。例えば、以下の Response クラスに対して完全なカスタムシリアライザを実装し、Ok サブクラスは直接表現し、Error サブクラスはエラーメッセージを持つオブジェクトで表現するようにします。

@Serializable(with = ResponseSerializer::class)
sealed class Response<out T> {
    data class Ok<out T>(val data: T) : Response<T>()
    data class Error(val message: String) : Response<Nothing>()
}

class ResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Response<T>> {
    override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) {
        element("Ok", buildClassSerialDescriptor("Ok") {
            element<String>("message")
        })
        element("Error", dataSerializer.descriptor)
    }

    override fun deserialize(decoder: Decoder): Response<T> {
        // Decoder -> JsonDecoder
        require(decoder is JsonDecoder) // this class can be decoded only by Json
        // JsonDecoder -> JsonElement
        val element = decoder.decodeJsonElement()
        // JsonElement -> value
        if (element is JsonObject && "error" in element)
            return Response.Error(element["error"]!!.jsonPrimitive.content)
        return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element))
    }

    override fun serialize(encoder: Encoder, value: Response<T>) {
        // Encoder -> JsonEncoder
        require(encoder is JsonEncoder) // This class can be encoded only by Json
        // value -> JsonElement
        val element = when (value) {
            is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data)
            is Response.Error -> buildJsonObject { put("error", value.message) }
        }
        // JsonElement -> JsonEncoder
        encoder.encodeJsonElement(element)
    }
}

 このシリアライズ可能な Response の実装で武装することで、シリアライズ可能なペイロードのデータを取得し、対応するレスポンスをシリアライズ/デシリアライズすることができます。

@Serializable
data class Project(val name: String)

fun main() {
    val responses = listOf(
        Response.Ok(Project("kotlinx.serialization")),
        Response.Error("Not found")
    )
    val string = Json.encodeToString(responses)
    println(string)
    println(Json.decodeFromString<List<Response<Project>>>(string))
}

フルコードはこちらから取得することができます。

 これにより、JSON出力における Response クラスの表現を細かく制御することができます。

[{"name":"kotlinx.serialization"},{"error":"Not found"}]
[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]

カスタムJSON属性の維持

 カスタムJSON固有のシリアライザの良い例としては、未知のJSONプロパティをすべてJsonObject型の専用フィールドにパックするデシリアライザがあります。 基本的な name プロパティと任意の詳細をフラット化した UnknownProject – クラスを追加してみましょう。

data class UnknownProject(val name: String, val details: JsonObject)

 しかし、デフォルトのプラグイン生成シリアライザでは、詳細を別のJSONオブジェクトにする必要があり、それは私たちが望むものではありません。これを緩和するために、Json 形式でしか使えないという事実を利用した独自のシリアライザを書くことができます。

object UnknownProjectSerializer : KSerializer<UnknownProject> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
        element<String>("name")
        element<JsonElement>("details")
    }

    override fun deserialize(decoder: Decoder): UnknownProject {
        // Cast to JSON-specific interface
        val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
        // Read the whole content as JSON
        val json = jsonInput.decodeJsonElement().jsonObject
        // Extract and remove name property
        val name = json.getValue("name").jsonPrimitive.content
        val details = json.toMutableMap()
        details.remove("name")
        return UnknownProject(name, JsonObject(details))
    }

    override fun serialize(encoder: Encoder, value: UnknownProject) {
        error("Serialization is not supported")
    }
}

 これで、フラット化されたJSONの詳細を UnknownProject として読み込むことができるようになった。

fun main() {
    println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
}

フルコードはこちらから取得することができます。

UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})

 次の章では、代替フォーマットとカスタムフォーマット(実験)を取り上げます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
8