@@ -38,7 +38,7 @@ public class AttachmentJsonConverter : JsonConverter<Attachment>
3838 // Route by type name — primary and most reliable method
3939 if ( IsType ( typeString , AttachmentTypeNames . Image ) )
4040 {
41- return JsonSerializer . Deserialize < PhotoAttachment > ( root . GetRawText ( ) , options ) ;
41+ return DeserializePhotoAttachment ( root , options ) ;
4242 }
4343
4444 if ( IsType ( typeString , AttachmentTypeNames . InlineKeyboard ) )
@@ -53,22 +53,22 @@ public class AttachmentJsonConverter : JsonConverter<Attachment>
5353
5454 if ( IsType ( typeString , AttachmentTypeNames . Contact ) )
5555 {
56- return JsonSerializer . Deserialize < ContactAttachment > ( root . GetRawText ( ) , options ) ;
56+ return DeserializeContactAttachment ( root , options ) ;
5757 }
5858
5959 if ( IsType ( typeString , AttachmentTypeNames . Video ) )
6060 {
61- return DeserializeAttachment < VideoAttachment > ( root , "video" , options ) ;
61+ return DeserializeMediaAttachment < VideoAttachment > ( root , "video" , options ) ;
6262 }
6363
6464 if ( IsType ( typeString , AttachmentTypeNames . Audio ) )
6565 {
66- return DeserializeAttachment < AudioAttachment > ( root , "audio" , options ) ;
66+ return DeserializeMediaAttachment < AudioAttachment > ( root , "audio" , options ) ;
6767 }
6868
6969 if ( IsType ( typeString , AttachmentTypeNames . File ) )
7070 {
71- return DeserializeAttachment < DocumentAttachment > ( root , "document" , options ) ;
71+ return DeserializeMediaAttachment < DocumentAttachment > ( root , "document" , options ) ;
7272 }
7373
7474 // Fallback for unknown types — use DocumentAttachment as it has the most generic fields
@@ -123,6 +123,119 @@ public override void Write(Utf8JsonWriter writer, Attachment value, JsonSerializ
123123 return JsonSerializer . Deserialize < T > ( root . GetRawText ( ) , options ) ;
124124 }
125125
126+ private static PhotoAttachment ? DeserializePhotoAttachment ( JsonElement root , JsonSerializerOptions options )
127+ {
128+ if ( root . TryGetProperty ( "payload" , out var payload ) && payload . ValueKind == JsonValueKind . Object )
129+ {
130+ var attachment = new PhotoAttachment ( ) ;
131+
132+ if ( payload . TryGetProperty ( "id" , out var idElement ) && idElement . ValueKind == JsonValueKind . Number && idElement . TryGetInt64 ( out var id ) )
133+ {
134+ attachment . Id = id ;
135+ }
136+ else if ( payload . TryGetProperty ( "photo_id" , out var photoIdElement ) && photoIdElement . ValueKind == JsonValueKind . Number && photoIdElement . TryGetInt64 ( out var photoId ) )
137+ {
138+ attachment . Id = photoId ;
139+ }
140+
141+ if ( payload . TryGetProperty ( "file_id" , out var fileIdElement ) && fileIdElement . ValueKind == JsonValueKind . String )
142+ {
143+ attachment . FileId = fileIdElement . GetString ( ) ?? string . Empty ;
144+ }
145+ else if ( payload . TryGetProperty ( "token" , out var tokenElement ) && tokenElement . ValueKind == JsonValueKind . String )
146+ {
147+ attachment . FileId = tokenElement . GetString ( ) ?? string . Empty ;
148+ }
149+
150+ if ( payload . TryGetProperty ( "width" , out var widthElement ) && widthElement . ValueKind == JsonValueKind . Number && widthElement . TryGetInt32 ( out var width ) )
151+ {
152+ attachment . Width = width ;
153+ }
154+
155+ if ( payload . TryGetProperty ( "height" , out var heightElement ) && heightElement . ValueKind == JsonValueKind . Number && heightElement . TryGetInt32 ( out var height ) )
156+ {
157+ attachment . Height = height ;
158+ }
159+
160+ if ( payload . TryGetProperty ( "file_size" , out var fileSizeElement ) && fileSizeElement . ValueKind == JsonValueKind . Number && fileSizeElement . TryGetInt64 ( out var fileSize ) )
161+ {
162+ attachment . FileSize = fileSize ;
163+ }
164+
165+ if ( payload . TryGetProperty ( "url" , out var urlElement ) && urlElement . ValueKind == JsonValueKind . String )
166+ {
167+ attachment . Url = urlElement . GetString ( ) ;
168+ }
169+
170+ return attachment ;
171+ }
172+
173+ if ( root . TryGetProperty ( "photo" , out var photo ) && photo . ValueKind == JsonValueKind . Object )
174+ {
175+ var attachment = JsonSerializer . Deserialize < PhotoAttachment > ( photo . GetRawText ( ) , options ) ;
176+ if ( attachment != null )
177+ {
178+ attachment . Type = AttachmentTypeNames . Image ;
179+ }
180+
181+ return attachment ;
182+ }
183+
184+ return JsonSerializer . Deserialize < PhotoAttachment > ( root . GetRawText ( ) , options ) ;
185+ }
186+
187+ private static ContactAttachment ? DeserializeContactAttachment ( JsonElement root , JsonSerializerOptions options )
188+ {
189+ if ( root . TryGetProperty ( "payload" , out var payload ) && payload . ValueKind == JsonValueKind . Object )
190+ {
191+ var attachment = JsonSerializer . Deserialize < ContactAttachment > ( payload . GetRawText ( ) , options ) ;
192+ if ( attachment != null )
193+ {
194+ // Ensure type is always set even when payload does not include it.
195+ attachment . Type = AttachmentTypeNames . Contact ;
196+ }
197+ return attachment ;
198+ }
199+
200+ return JsonSerializer . Deserialize < ContactAttachment > ( root . GetRawText ( ) , options ) ;
201+ }
202+
203+ private static T ? DeserializeMediaAttachment < T > ( JsonElement root , string payloadPropertyName , JsonSerializerOptions options )
204+ where T : Attachment
205+ {
206+ if ( root . TryGetProperty ( "payload" , out var payload ) && payload . ValueKind == JsonValueKind . Object )
207+ {
208+ var attachment = JsonSerializer . Deserialize < T > ( payload . GetRawText ( ) , options ) ;
209+ if ( attachment == null )
210+ {
211+ return null ;
212+ }
213+
214+ switch ( attachment )
215+ {
216+ case AudioAttachment audio when string . IsNullOrWhiteSpace ( audio . FileId )
217+ && payload . TryGetProperty ( "token" , out var audioToken )
218+ && audioToken . ValueKind == JsonValueKind . String :
219+ audio . FileId = audioToken . GetString ( ) ?? string . Empty ;
220+ break ;
221+ case VideoAttachment video when string . IsNullOrWhiteSpace ( video . FileId )
222+ && payload . TryGetProperty ( "token" , out var videoToken )
223+ && videoToken . ValueKind == JsonValueKind . String :
224+ video . FileId = videoToken . GetString ( ) ?? string . Empty ;
225+ break ;
226+ case DocumentAttachment document when string . IsNullOrWhiteSpace ( document . FileId )
227+ && payload . TryGetProperty ( "token" , out var documentToken )
228+ && documentToken . ValueKind == JsonValueKind . String :
229+ document . FileId = documentToken . GetString ( ) ?? string . Empty ;
230+ break ;
231+ }
232+
233+ return attachment ;
234+ }
235+
236+ return DeserializeAttachment < T > ( root , payloadPropertyName , options ) ;
237+ }
238+
126239 private static bool IsType ( string ? actualType , string expectedType )
127240 {
128241 return ! string . IsNullOrWhiteSpace ( actualType ) &&
0 commit comments