Serialization for machines
When structures are serialized to JSON to be transferred over the wire, there may be servers that expect null
values to be omitted, possibly to save bandwidth. Right now, there is no way in serde
to omit null values.
As a practical example, imagine someone trying to upload a video to youtube:
$ youtube3 --debug videos insert \
-r snippet \
title="Google APIs for Rust: Developer Diary #2 [Making CLIs]" \
description="TBD" \
tags="Google APIs, Google, rust-lang, Diary, OSS" \
category-id=22 \
..status privacy-status=private \
embeddable=true \
license=youtube \
-u resumable ~/Movies/youtube-originals/Google\ APIs\ for\ Rust\ -\ Using\ youtube3\ to\ upload\ a\ video.mov application/octet-stream
Which yields the following error:
Bad Requst (400): Invalid value for: null is not a valid value
As well as the following dialogue between client and server:
POST /resumable/upload/youtube/v3/videos?part=status%2Csnippet&alt=json&uploadType=resumable HTTP/1.1
User-Agent: google-api-rust-client/0.1.6
Host: www.googleapis.com
Transfer-Encoding: chunked
Content-Type: application/json
X-Upload-Content-Type: application/octet-stream
Authorization: Bearer ya29.YwHqwwVjMrn7y_qO7d6RR5KeowbDJFO_2mLk5pTPs9iJZP0k3DEHUm6E4xkOv3pw5oEhX3GBjI-H4A
33C
{"status":{"license":"youtube","embeddable":true,"privacyStatus":"private","publishAt":null,"publicStatsViewable":null,"uploadStatus":null,"rejectionReason":null,"failureReason":null},"topicDetails":null,"monetizationDetails":null,"suggestions":null,"ageGating":null,"fileDetails":null,"player":null,"id":null,"localizations":null,"liveStreamingDetails":null,"snippet":{"description":"TBD","tags":["Google APIs, Google, rust-lang, Diary, OSS"],"channelId":null,"defaultLanguage":null,"liveBroadcastContent":null,"publishedAt":null,"thumbnails":null,"title":"Google APIs for Rust: Developer Diary #2 [Making CLIs]","categoryId":"22","localized":null,"channelTitle":null},"kind":null,"statistics":null,"projectDetails":null,"conversionPings":null,"processingDetails":null,"etag":null,"contentDetails":null,"recordingDetails":null}
0
HTTP/1.1 400 Bad Request
Vary: Origin
Vary: X-Origin
Content-Type: application/json; charset=UTF-8
Content-Length: 234
Date: Tue, 28 Apr 2015 06:31:59 GMT
Server: UploadServer ("Built on Apr 20 2015 22:37:13 (1429594633)")
Alternate-Protocol: 443:quic,p=1
{
"error": {
"errors": [
{
"domain": "global",
"reason": "invalid",
"message": "Invalid value for: null is not a valid value"
}
],
"code": 400,
"message": "Invalid value for: null is not a valid value"
}
}
As you can see, the request contains null
values which are not allowed.
To further stress the importance of this feature, have a look at the respective Go implementation ...
type AccessPolicy struct {
// Allowed: The value of allowed indicates whether the access to the
// policy is allowed or denied by default.
Allowed bool `json:"allowed,omitempty"`
// Exception: A list of region codes that identify countries where the
// default policy do not apply.
Exception []string `json:"exception,omitempty"`
}
... where the marker omitempty
will prevent it to be serialized if unset.
You can try it yourself using the youtube3
program, which can be downloaded here.
Serialization for human consumption
Right now there is exactly one method to get 'pretty', i.e. more human-friendly json output. It provides no option to specify how exactly that could be done.
The most prominent one to me would be a setting for whether or not to ignore null
values. Other options could be the indentation string to use, e.g. \t
or
.
Motivation
When printing the server response of any of the various google apis using a generated command-line interface, simple invocation yield results like this:
$ discovery1 apis get-rest discovery v1
{
"protocol": "rest",
"methods": null,
"labels": null,
"kind": "discovery#restDescription",
"canonicalName": null,
"ownerName": "Google",
"documentationLink": "https://developers.google.com/discovery/",
"auth": null,
"packagePath": null,
"batchPath": "batch",
"id": "discovery:v1",
"features": null,
"ownerDomain": "google.com",
"rootUrl": "https://www.googleapis.com/",
"name": "discovery",
"parameters": {
"key": {
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
"format": null,
"enum": null,
"variant": null,
"enumDescriptions": null,
"readOnly": null,
"minimum": null,
"repeated": null,
"id": null,
"$ref": null,
"default": null,
"items": null,
"required": null,
"maximum": null,
"properties": null,
"location": "query",
"pattern": null,
"additionalProperties": null,
"type": "string",
"annotations": null
},
"userIp": {
"description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.",
"format": null,
"enum": null,
"variant": null,
"enumDescriptions": null,
"readOnly": null,
"minimum": null,
"repeated": null,
"id": null,
"$ref": null,
"default": null,
"items": null,
"required": null,
"maximum": null,
"properties": null,
"location": "query",
"pattern": null,
"additionalProperties": null,
"type": "string",
"annotations": null
},
[...]
"revision": null
}
The above should look like this:
$ discovery1 apis get-rest discovery v1
{
"kind": "discovery#restDescription",
"etag": "\"ye6orv2F-1npMW3u9suM3a7C5Bo/rJ-Wlqqs_yJDjtCFAIylPtmqXPY\"",
"discoveryVersion": "v1",
"id": "discovery:v1",
"name": "discovery",
"version": "v1",
"title": "APIs Discovery Service",
"description": "Lets you discover information about other Google APIs, such as what APIs are available, the resource and method details for each API.",
"ownerDomain": "google.com",
"ownerName": "Google",
"icons": {
"x16": "http://www.google.com/images/icons/feature/filing_cabinet_search-g16.png",
"x32": "http://www.google.com/images/icons/feature/filing_cabinet_search-g32.png"
},
"documentationLink": "https://developers.google.com/discovery/",
"protocol": "rest",
"baseUrl": "https://www.googleapis.com/discovery/v1/",
"basePath": "/discovery/v1/",
"rootUrl": "https://www.googleapis.com/",
"servicePath": "discovery/v1/",
"batchPath": "batch",
"parameters": {
"alt": {
"type": "string",
"description": "Data format for the response.",
"default": "json",
"enum": [
"json"
],
"enumDescriptions": [
"Responses with Content-Type of application/json"
],
"location": "query"
},
"fields": {
"type": "string",
"description": "Selector specifying which fields to include in a partial response.",
"location": "query"
},
"key": {
"type": "string",
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
"location": "query"
},
"oauth_token": {
"type": "string",
"description": "OAuth 2.0 token for the current user.",
"location": "query"
},
"prettyPrint": {
"type": "boolean",
"description": "Returns response with indentations and line breaks.",
"default": "true",
"location": "query"
},
"quotaUser": {
"type": "string",
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.",
"location": "query"
},
"userIp": {
"type": "string",
"description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.",
"location": "query"
}
},
"schemas": {
"DirectoryList": {
"id": "DirectoryList",
"type": "object",
"properties": {
"discoveryVersion": {
"type": "string",
"description": "Indicate the version of the Discovery API used to generate this doc.",
"default": "v1"
},
"items": {
"type": "array",
"description": "The individual directory entries. One entry per api/version pair.",
"items": {
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "The description of this API."
},
"discoveryLink": {
"type": "string",
"description": "A link to the discovery document."
},
"discoveryRestUrl": {
"type": "string",
"description": "The URL for the discovery REST document."
},
"documentationLink": {
"type": "string",
"description": "A link to human readable documentation for the API."
},
"icons": {
"type": "object",
"description": "Links to 16x16 and 32x32 icons representing the API.",
"properties": {
"x16": {
"type": "string",
"description": "The URL of the 16x16 icon."
},
"x32": {
"type": "string",
"description": "The URL of the 32x32 icon."
}
}
},
"id": {
"type": "string",
"description": "The id of this API."
},
"kind": {
"type": "string",
"description": "The kind for this response.",
"default": "discovery#directoryItem"
},
"labels": {
"type": "array",
"description": "Labels for the status of this API, such as labs or deprecated.",
"items": {
"type": "string"
}
},
"name": {
"type": "string",
"description": "The name of the API."
},
"preferred": {
"type": "boolean",
"description": "True if this version is the preferred version to use."
},
"title": {
"type": "string",
"description": "The title of this API."
},
"version": {
"type": "string",
"description": "The version of the API."
}
}
}
},
"kind": {
"type": "string",
"description": "The kind for this response.",
"default": "discovery#directoryList"
}
}
},
[...]
}
enhancement