{"action":"created","issue":{"url":"https://api.github.com/repos/serde-rs/serde/issues/65","repository_url":"https://api.github.com/repos/serde-rs/serde","labels_url":"https://api.github.com/repos/serde-rs/serde/issues/65/labels{/name}","comments_url":"https://api.github.com/repos/serde-rs/serde/issues/65/comments","events_url":"https://api.github.com/repos/serde-rs/serde/issues/65/events","html_url":"https://github.com/serde-rs/serde/issues/65","id":70701435,"node_id":"MDU6SXNzdWU3MDcwMTQzNQ==","number":65,"title":"more flexible JSON serialization","user":{"login":"Byron","id":63622,"node_id":"MDQ6VXNlcjYzNjIy","avatar_url":"https://avatars2.githubusercontent.com/u/63622?v=4","gravatar_id":"","url":"https://api.github.com/users/Byron","html_url":"https://github.com/Byron","followers_url":"https://api.github.com/users/Byron/followers","following_url":"https://api.github.com/users/Byron/following{/other_user}","gists_url":"https://api.github.com/users/Byron/gists{/gist_id}","starred_url":"https://api.github.com/users/Byron/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Byron/subscriptions","organizations_url":"https://api.github.com/users/Byron/orgs","repos_url":"https://api.github.com/users/Byron/repos","events_url":"https://api.github.com/users/Byron/events{/privacy}","received_events_url":"https://api.github.com/users/Byron/received_events","type":"User","site_admin":false},"labels":[{"id":65683853,"node_id":"MDU6TGFiZWw2NTY4Mzg1Mw==","url":"https://api.github.com/repos/serde-rs/serde/labels/enhancement","name":"enhancement","color":"84b6eb","default":true}],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":27,"created_at":"2015-04-24T14:17:59Z","updated_at":"2018-09-17T20:39:28Z","closed_at":"2016-06-13T06:41:10Z","author_association":"CONTRIBUTOR","body":"# Serialization for machines\n\nWhen 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.\n\nAs a practical example, imagine someone trying to upload a video to youtube:\n\n``` bash\n$ youtube3 --debug videos insert \\\n-r snippet \\\ntitle=\"Google APIs for Rust: Developer Diary #2 [Making CLIs]\" \\\ndescription=\"TBD\" \\\ntags=\"Google APIs, Google, rust-lang, Diary, OSS\" \\\ncategory-id=22 \\\n..status privacy-status=private \\\nembeddable=true \\\nlicense=youtube \\\n-u resumable ~/Movies/youtube-originals/Google\\ APIs\\ for\\ Rust\\ -\\ Using\\ youtube3\\ to\\ upload\\ a\\ video.mov application/octet-stream\n```\n\nWhich yields the following error:\n\n``` bash\nBad Requst (400): Invalid value for: null is not a valid value\n```\n\nAs well as the following dialogue between client and server:\n\n```\nPOST /resumable/upload/youtube/v3/videos?part=status%2Csnippet&alt=json&uploadType=resumable HTTP/1.1\nUser-Agent: google-api-rust-client/0.1.6\nHost: www.googleapis.com\nTransfer-Encoding: chunked\nContent-Type: application/json\nX-Upload-Content-Type: application/octet-stream\nAuthorization: Bearer ya29.YwHqwwVjMrn7y_qO7d6RR5KeowbDJFO_2mLk5pTPs9iJZP0k3DEHUm6E4xkOv3pw5oEhX3GBjI-H4A\n\n33C\n{\"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}\n0\n\nHTTP/1.1 400 Bad Request\nVary: Origin\nVary: X-Origin\nContent-Type: application/json; charset=UTF-8\nContent-Length: 234\nDate: Tue, 28 Apr 2015 06:31:59 GMT\nServer: UploadServer (\"Built on Apr 20 2015 22:37:13 (1429594633)\")\nAlternate-Protocol: 443:quic,p=1\n\n{\n \"error\": {\n  \"errors\": [\n   {\n    \"domain\": \"global\",\n    \"reason\": \"invalid\",\n    \"message\": \"Invalid value for: null is not a valid value\"\n   }\n  ],\n  \"code\": 400,\n  \"message\": \"Invalid value for: null is not a valid value\"\n }\n}\n```\n\nAs you can see, the request contains `null` values which are not allowed.\n\nTo further stress the importance of this feature, have a look at the respective Go implementation ...\n\n``` Go\ntype AccessPolicy struct {\n    // Allowed: The value of allowed indicates whether the access to the\n    // policy is allowed or denied by default.\n    Allowed bool `json:\"allowed,omitempty\"`\n\n    // Exception: A list of region codes that identify countries where the\n    // default policy do not apply.\n    Exception []string `json:\"exception,omitempty\"`\n}\n```\n\n... where the marker `omitempty` will prevent it to be serialized if unset.\n\nYou can try it yourself using the `youtube3` program, which can be [downloaded here](https://github.com/Byron/google-apis-rs/releases).\n# Serialization for human consumption\n\nRight now there is exactly [one method](http://erickt.github.io/rust-serde/serde/json/ser/fn.to_writer_pretty.html) to get 'pretty', i.e. more human-friendly json output. It provides no option to specify how exactly that could be done.\n\nThe 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 `  `.\n## Motivation\n\nWhen printing the server response of any of the various [google apis](https://github.com/Byron/google-apis-rs) using a generated command-line interface, simple invocation yield results like this:\n\n``` json\n$ discovery1 apis get-rest discovery v1\n{\n  \"protocol\": \"rest\",\n  \"methods\": null,\n  \"labels\": null,\n  \"kind\": \"discovery#restDescription\",\n  \"canonicalName\": null,\n  \"ownerName\": \"Google\",\n  \"documentationLink\": \"https://developers.google.com/discovery/\",\n  \"auth\": null,\n  \"packagePath\": null,\n  \"batchPath\": \"batch\",\n  \"id\": \"discovery:v1\",\n  \"features\": null,\n  \"ownerDomain\": \"google.com\",\n  \"rootUrl\": \"https://www.googleapis.com/\",\n  \"name\": \"discovery\",\n  \"parameters\": {\n    \"key\": {\n      \"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.\",\n      \"format\": null,\n      \"enum\": null,\n      \"variant\": null,\n      \"enumDescriptions\": null,\n      \"readOnly\": null,\n      \"minimum\": null,\n      \"repeated\": null,\n      \"id\": null,\n      \"$ref\": null,\n      \"default\": null,\n      \"items\": null,\n      \"required\": null,\n      \"maximum\": null,\n      \"properties\": null,\n      \"location\": \"query\",\n      \"pattern\": null,\n      \"additionalProperties\": null,\n      \"type\": \"string\",\n      \"annotations\": null\n    },\n    \"userIp\": {\n      \"description\": \"IP address of the site where the request originates. Use this if you want to enforce per-user limits.\",\n      \"format\": null,\n      \"enum\": null,\n      \"variant\": null,\n      \"enumDescriptions\": null,\n      \"readOnly\": null,\n      \"minimum\": null,\n      \"repeated\": null,\n      \"id\": null,\n      \"$ref\": null,\n      \"default\": null,\n      \"items\": null,\n      \"required\": null,\n      \"maximum\": null,\n      \"properties\": null,\n      \"location\": \"query\",\n      \"pattern\": null,\n      \"additionalProperties\": null,\n      \"type\": \"string\",\n      \"annotations\": null\n    },\n  [...]\n  \"revision\": null\n}\n```\n\nThe above should look like this:\n\n``` json\n$ discovery1 apis get-rest discovery v1\n{\n \"kind\": \"discovery#restDescription\",\n \"etag\": \"\\\"ye6orv2F-1npMW3u9suM3a7C5Bo/rJ-Wlqqs_yJDjtCFAIylPtmqXPY\\\"\",\n \"discoveryVersion\": \"v1\",\n \"id\": \"discovery:v1\",\n \"name\": \"discovery\",\n \"version\": \"v1\",\n \"title\": \"APIs Discovery Service\",\n \"description\": \"Lets you discover information about other Google APIs, such as what APIs are available, the resource and method details for each API.\",\n \"ownerDomain\": \"google.com\",\n \"ownerName\": \"Google\",\n \"icons\": {\n  \"x16\": \"http://www.google.com/images/icons/feature/filing_cabinet_search-g16.png\",\n  \"x32\": \"http://www.google.com/images/icons/feature/filing_cabinet_search-g32.png\"\n },\n \"documentationLink\": \"https://developers.google.com/discovery/\",\n \"protocol\": \"rest\",\n \"baseUrl\": \"https://www.googleapis.com/discovery/v1/\",\n \"basePath\": \"/discovery/v1/\",\n \"rootUrl\": \"https://www.googleapis.com/\",\n \"servicePath\": \"discovery/v1/\",\n \"batchPath\": \"batch\",\n \"parameters\": {\n  \"alt\": {\n   \"type\": \"string\",\n   \"description\": \"Data format for the response.\",\n   \"default\": \"json\",\n   \"enum\": [\n    \"json\"\n   ],\n   \"enumDescriptions\": [\n    \"Responses with Content-Type of application/json\"\n   ],\n   \"location\": \"query\"\n  },\n  \"fields\": {\n   \"type\": \"string\",\n   \"description\": \"Selector specifying which fields to include in a partial response.\",\n   \"location\": \"query\"\n  },\n  \"key\": {\n   \"type\": \"string\",\n   \"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.\",\n   \"location\": \"query\"\n  },\n  \"oauth_token\": {\n   \"type\": \"string\",\n   \"description\": \"OAuth 2.0 token for the current user.\",\n   \"location\": \"query\"\n  },\n  \"prettyPrint\": {\n   \"type\": \"boolean\",\n   \"description\": \"Returns response with indentations and line breaks.\",\n   \"default\": \"true\",\n   \"location\": \"query\"\n  },\n  \"quotaUser\": {\n   \"type\": \"string\",\n   \"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.\",\n   \"location\": \"query\"\n  },\n  \"userIp\": {\n   \"type\": \"string\",\n   \"description\": \"IP address of the site where the request originates. Use this if you want to enforce per-user limits.\",\n   \"location\": \"query\"\n  }\n },\n \"schemas\": {\n  \"DirectoryList\": {\n   \"id\": \"DirectoryList\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"discoveryVersion\": {\n     \"type\": \"string\",\n     \"description\": \"Indicate the version of the Discovery API used to generate this doc.\",\n     \"default\": \"v1\"\n    },\n    \"items\": {\n     \"type\": \"array\",\n     \"description\": \"The individual directory entries. One entry per api/version pair.\",\n     \"items\": {\n      \"type\": \"object\",\n      \"properties\": {\n       \"description\": {\n        \"type\": \"string\",\n        \"description\": \"The description of this API.\"\n       },\n       \"discoveryLink\": {\n        \"type\": \"string\",\n        \"description\": \"A link to the discovery document.\"\n       },\n       \"discoveryRestUrl\": {\n        \"type\": \"string\",\n        \"description\": \"The URL for the discovery REST document.\"\n       },\n       \"documentationLink\": {\n        \"type\": \"string\",\n        \"description\": \"A link to human readable documentation for the API.\"\n       },\n       \"icons\": {\n        \"type\": \"object\",\n        \"description\": \"Links to 16x16 and 32x32 icons representing the API.\",\n        \"properties\": {\n         \"x16\": {\n          \"type\": \"string\",\n          \"description\": \"The URL of the 16x16 icon.\"\n         },\n         \"x32\": {\n          \"type\": \"string\",\n          \"description\": \"The URL of the 32x32 icon.\"\n         }\n        }\n       },\n       \"id\": {\n        \"type\": \"string\",\n        \"description\": \"The id of this API.\"\n       },\n       \"kind\": {\n        \"type\": \"string\",\n        \"description\": \"The kind for this response.\",\n        \"default\": \"discovery#directoryItem\"\n       },\n       \"labels\": {\n        \"type\": \"array\",\n        \"description\": \"Labels for the status of this API, such as labs or deprecated.\",\n        \"items\": {\n         \"type\": \"string\"\n        }\n       },\n       \"name\": {\n        \"type\": \"string\",\n        \"description\": \"The name of the API.\"\n       },\n       \"preferred\": {\n        \"type\": \"boolean\",\n        \"description\": \"True if this version is the preferred version to use.\"\n       },\n       \"title\": {\n        \"type\": \"string\",\n        \"description\": \"The title of this API.\"\n       },\n       \"version\": {\n        \"type\": \"string\",\n        \"description\": \"The version of the API.\"\n       }\n      }\n     }\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"The kind for this response.\",\n     \"default\": \"discovery#directoryList\"\n    }\n   }\n  },\n [...]\n}\n```\n"},"comment":{"url":"https://api.github.com/repos/serde-rs/serde/issues/comments/422162821","html_url":"https://github.com/serde-rs/serde/issues/65#issuecomment-422162821","issue_url":"https://api.github.com/repos/serde-rs/serde/issues/65","id":422162821,"node_id":"MDEyOklzc3VlQ29tbWVudDQyMjE2MjgyMQ==","user":{"login":"xtlsheep","id":40476544,"node_id":"MDQ6VXNlcjQwNDc2NTQ0","avatar_url":"https://avatars2.githubusercontent.com/u/40476544?v=4","gravatar_id":"","url":"https://api.github.com/users/xtlsheep","html_url":"https://github.com/xtlsheep","followers_url":"https://api.github.com/users/xtlsheep/followers","following_url":"https://api.github.com/users/xtlsheep/following{/other_user}","gists_url":"https://api.github.com/users/xtlsheep/gists{/gist_id}","starred_url":"https://api.github.com/users/xtlsheep/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xtlsheep/subscriptions","organizations_url":"https://api.github.com/users/xtlsheep/orgs","repos_url":"https://api.github.com/users/xtlsheep/repos","events_url":"https://api.github.com/users/xtlsheep/events{/privacy}","received_events_url":"https://api.github.com/users/xtlsheep/received_events","type":"User","site_admin":false},"created_at":"2018-09-17T20:39:28Z","updated_at":"2018-09-17T20:39:28Z","author_association":"NONE","body":"So there is still NOT a macro to cover a whole struct with non-option and option attributes? Do I really have to add #[serde(skip_serializing_if = \"Option::is_none\")] before every option key?"}}
{"actor":{"display_login":"xtlsheep"}}