{"record":{"id":"f57a0ca1762ecbdb","repo":"grpc/grpc-go","slug":"pemfile-json-unmarshal-s-failed-v","errorCode":null,"errorMessage":"pemfile: json.Unmarshal(%s) failed: %v","messagePattern":"pemfile: json\\.Unmarshal\\((.+?)\\) failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"credentials/tls/certprovider/pemfile/builder.go","lineNumber":74,"sourceCode":"}\n\nfunc (p *pluginBuilder) Name() string {\n\treturn PluginName\n}\n\nfunc pluginConfigFromJSON(jd json.RawMessage) (Options, error) {\n\t// The only difference between this anonymous struct and the Options struct\n\t// is that the refresh_interval is represented here as a duration proto,\n\t// while in the latter a time.Duration is used.\n\tcfg := &struct {\n\t\tCertificateFile          string          `json:\"certificate_file,omitempty\"`\n\t\tPrivateKeyFile           string          `json:\"private_key_file,omitempty\"`\n\t\tCACertificateFile        string          `json:\"ca_certificate_file,omitempty\"`\n\t\tSPIFFETrustBundleMapFile string          `json:\"spiffe_trust_bundle_map_file,omitempty\"`\n\t\tRefreshInterval          json.RawMessage `json:\"refresh_interval,omitempty\"`\n\t}{}\n\tif err := json.Unmarshal(jd, cfg); err != nil {\n\t\treturn Options{}, fmt.Errorf(\"pemfile: json.Unmarshal(%s) failed: %v\", string(jd), err)\n\t}\n\tif !envconfig.XDSSPIFFEEnabled {\n\t\tcfg.SPIFFETrustBundleMapFile = \"\"\n\t}\n\n\topts := Options{\n\t\tCertFile:            cfg.CertificateFile,\n\t\tKeyFile:             cfg.PrivateKeyFile,\n\t\tRootFile:            cfg.CACertificateFile,\n\t\tSPIFFEBundleMapFile: cfg.SPIFFETrustBundleMapFile,\n\t\t// Refresh interval is the only field in the configuration for which we\n\t\t// support a default value. We cannot possibly have valid defaults for\n\t\t// file paths to watch. Also, it is valid to specify an empty path for\n\t\t// some of those fields if the user does not want to watch them.\n\t\tRefreshDuration: defaultRefreshInterval,\n\t}\n\tif cfg.RefreshInterval != nil {\n\t\tdur := &durationpb.Duration{}","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/credentials/tls/certprovider/pemfile/builder.go#L56-L92","documentation":"The pemfile (file_watcher) certificate provider plugin could not JSON-unmarshal the configuration blob passed to ParseConfig into its expected struct (certificate_file, private_key_file, ca_certificate_file, spiffe_trust_bundle_map_file, refresh_interval). The wrapper passes the raw bytes verbatim into the error so you can see exactly what failed to parse. It is thrown at config-parse time, before any provider is built, so it surfaces during grpc.Dial/Server startup when an xDS or programmatic config supplies malformed JSON.","triggerScenarios":"Calling certprovider.ParseConfig(\"file_watcher\", []byte(`{bad json`)) or providing a file_watcher config via xDS/bootstrap where a field has the wrong JSON type (e.g. \"certificate_file\": 123) or a structural error such as a trailing comma, unescaped quote, or unclosed brace.","commonSituations":"Hand-editing the GRPC_XDS_BOOTSTRAP file or a JSON config string and introducing a typo; feeding a Go map or struct into ParseConfig instead of json.RawMessage; copy-pasting a config that uses comments (//) which json.Unmarshal rejects.","solutions":["Validate the offending JSON printed in the %s slot with a linter or `jq -e .` before passing it to ParseConfig.","Ensure the value passed to ParseConfig is a json.RawMessage whose contents are a complete JSON object; do not pass a Go struct, map, or empty slice.","Construct the config from a typed Go struct and re-marshal it with json.Marshal to guarantee validity before handing it to the provider.","Check that every field value matches its documented JSON type (all four *_file fields are strings; refresh_interval is a proto-JSON Duration)."],"exampleFix":"// before\ncfg := []byte(`{ \"certificate_file\": \"server.crt\", \"private_key_file\": \"server.key\", }`) // trailing comma -> error\ncertprovider.ParseConfig(\"file_watcher\", cfg)\n\n// after\ncfg := []byte(`{ \"certificate_file\": \"server.crt\", \"private_key_file\": \"server.key\" }`)\nif _, err := certprovider.ParseConfig(\"file_watcher\", cfg); err != nil { log.Fatal(err) }","handlingStrategy":"validation","validationCode":"func validFileWatcherJSON(b []byte) error {\n    var raw map[string]any\n    return json.Unmarshal(b, &raw) // nil == structurally valid JSON\n}\n// call before certprovider.ParseConfig(\"file_watcher\", rawJSON)","typeGuard":"func isRawMessage(v any) bool {\n    _, ok := v.(json.RawMessage)\n    return ok\n}","tryCatchPattern":"bc, err := certprovider.ParseConfig(\"file_watcher\", rawJSON)\nif err != nil {\n    return fmt.Errorf(\"invalid file_watcher config: %w\", err)\n}","preventionTips":["Always pass json.RawMessage (not a Go struct/map) into certprovider.ParseConfig.","Generate config from typed structs via json.Marshal rather than hand-writing JSON strings.","Lint config files with jq in CI before deploying."],"tags":["grpc","config","pemfile","json","certprovider","tls"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}