Coverage Report

Created: 2026-03-26 06:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-protobuf/Sources/SwiftProtobuf/Message+JSONAdditions.swift
Line
Count
Source
1
// Sources/SwiftProtobuf/Message+JSONAdditions.swift - JSON format primitive types
2
//
3
// Copyright (c) 2014 - 2016 Apple Inc. and the project authors
4
// Licensed under Apache License v2.0 with Runtime Library Exception
5
//
6
// See LICENSE.txt for license information:
7
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
8
//
9
// -----------------------------------------------------------------------------
10
///
11
/// Extensions to `Message` to support JSON encoding/decoding.
12
///
13
// -----------------------------------------------------------------------------
14
15
#if canImport(FoundationEssentials)
16
import FoundationEssentials
17
#else
18
import Foundation
19
#endif
20
21
/// JSON encoding and decoding methods for messages.
22
extension Message {
23
    /// Returns a string containing the JSON serialization of the message.
24
    ///
25
    /// Unlike binary encoding, presence of required fields is not enforced when
26
    /// serializing to JSON.
27
    ///
28
    /// - Returns: A string containing the JSON serialization of the message.
29
    /// - Parameters:
30
    ///   - options: The JSONEncodingOptions to use.
31
    /// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
32
    public func jsonString(
33
        options: JSONEncodingOptions = JSONEncodingOptions()
34
1.61M
    ) throws -> String {
35
1.61M
        if let m = self as? (any _CustomJSONCodable) {
36
827k
            return try m.encodedJSONString(options: options)
37
827k
        }
38
786k
        let data: [UInt8] = try jsonUTF8Bytes(options: options)
39
786k
        return String(decoding: data, as: UTF8.self)
40
1.61M
    }
41
42
    /// Returns a `SwiftProtobufContiguousBytes` containing the UTF-8 JSON serialization of the message.
43
    ///
44
    /// Unlike binary encoding, presence of required fields is not enforced when
45
    /// serializing to JSON.
46
    ///
47
    /// - Returns: A `SwiftProtobufContiguousBytes` containing the JSON serialization of the message.
48
    /// - Parameters:
49
    ///   - options: The JSONEncodingOptions to use.
50
    /// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
51
    public func jsonUTF8Bytes<Bytes: SwiftProtobufContiguousBytes>(
52
        options: JSONEncodingOptions = JSONEncodingOptions()
53
92.9k
    ) throws -> Bytes {
54
92.9k
        if let m = self as? (any _CustomJSONCodable) {
55
0
            let string = try m.encodedJSONString(options: options)
56
0
            return Bytes(string.utf8)
57
92.9k
        }
58
92.9k
        var visitor = try JSONEncodingVisitor(type: Self.self, options: options)
59
92.9k
        visitor.startObject(message: self)
60
92.9k
        try traverse(visitor: &visitor)
61
92.9k
        visitor.endObject()
62
92.9k
        return Bytes(visitor.dataResult)
63
92.9k
    }
64
65
    /// Creates a new message by decoding the given string containing a
66
    /// serialized message in JSON format.
67
    ///
68
    /// - Parameter jsonString: The JSON-formatted string to decode.
69
    /// - Parameter options: The JSONDecodingOptions to use.
70
    /// - Throws: ``SwiftProtobufError`` or ``JSONDecodingError`` if decoding fails.
71
    public init(
72
        jsonString: String,
73
        options: JSONDecodingOptions = JSONDecodingOptions()
74
0
    ) throws {
75
0
        try self.init(jsonString: jsonString, extensions: nil, options: options)
76
0
    }
77
78
    /// Creates a new message by decoding the given string containing a
79
    /// serialized message in JSON format.
80
    ///
81
    /// - Parameter jsonString: The JSON-formatted string to decode.
82
    /// - Parameter extensions: An ExtensionMap for looking up extensions by name
83
    /// - Parameter options: The JSONDecodingOptions to use.
84
    /// - Throws: ``SwiftProtobufError`` or ``JSONDecodingError`` if decoding fails.
85
    public init(
86
        jsonString: String,
87
        extensions: (any ExtensionMap)? = nil,
88
        options: JSONDecodingOptions = JSONDecodingOptions()
89
0
    ) throws {
90
0
        if jsonString.isEmpty {
91
0
            throw JSONDecodingError.truncated
92
0
        }
93
0
        if let data = jsonString.data(using: String.Encoding.utf8) {
94
0
            try self.init(jsonUTF8Bytes: data, extensions: extensions, options: options)
95
0
        } else {
96
0
            throw JSONDecodingError.truncated
97
0
        }
98
0
    }
99
100
    /// Creates a new message by decoding the given `SwiftProtobufContiguousBytes`
101
    /// containing a serialized message in JSON format, interpreting the data as UTF-8 encoded
102
    /// text.
103
    ///
104
    /// - Parameter jsonUTF8Bytes: The JSON-formatted data to decode, represented
105
    ///   as UTF-8 encoded text.
106
    /// - Parameter options: The JSONDecodingOptions to use.
107
    /// - Throws: ``SwiftProtobufError`` or ``JSONDecodingError`` if decoding fails.
108
    public init<Bytes: SwiftProtobufContiguousBytes>(
109
        jsonUTF8Bytes: Bytes,
110
        options: JSONDecodingOptions = JSONDecodingOptions()
111
0
    ) throws {
112
0
        try self.init(jsonUTF8Bytes: jsonUTF8Bytes, extensions: nil, options: options)
113
0
    }
114
115
    /// Creates a new message by decoding the given `SwiftProtobufContiguousBytes`
116
    /// containing a serialized message in JSON format, interpreting the data as UTF-8 encoded
117
    /// text.
118
    ///
119
    /// - Parameter jsonUTF8Bytes: The JSON-formatted data to decode, represented
120
    ///   as UTF-8 encoded text.
121
    /// - Parameter extensions: The extension map to use with this decode
122
    /// - Parameter options: The JSONDecodingOptions to use.
123
    /// - Throws: ``SwiftProtobufError`` or ``JSONDecodingError`` if decoding fails.
124
    public init<Bytes: SwiftProtobufContiguousBytes>(
125
        jsonUTF8Bytes: Bytes,
126
        extensions: (any ExtensionMap)? = nil,
127
        options: JSONDecodingOptions = JSONDecodingOptions()
128
40.9k
    ) throws {
129
40.9k
        self.init()
130
40.9k
        try jsonUTF8Bytes.withUnsafeBytes { (body: UnsafeRawBufferPointer) in
131
40.9k
            // Empty input is valid for binary, but not for JSON.
132
40.9k
            guard body.count > 0 else {
133
7
                throw JSONDecodingError.truncated
134
40.9k
            }
135
40.9k
            var decoder = JSONDecoder(
136
40.9k
                source: body,
137
40.9k
                options: options,
138
40.9k
                messageType: Self.self,
139
40.9k
                extensions: extensions
140
40.9k
            )
141
40.9k
            if decoder.scanner.skipOptionalNull() {
142
19
                if let customCodable = Self.self as? any _CustomJSONCodable.Type,
143
19
                    let message = try customCodable.decodedFromJSONNull()
144
19
                {
145
0
                    self = message as! Self
146
19
                } else {
147
19
                    throw JSONDecodingError.illegalNull
148
19
                }
149
40.8k
            } else {
150
40.8k
                try decoder.decodeFullObject(message: &self)
151
15.4k
            }
152
15.4k
            if !decoder.scanner.complete {
153
37
                throw JSONDecodingError.trailingGarbage
154
15.4k
            }
155
15.4k
        }
156
15.4k
    }
157
}